Compare commits

..
Author SHA1 Message Date
TaikoZhongandClaude Fable 5.1 9e35c05a27 Clip players: draw mirrored leaves as a horizontal flip
A leaf placed with a negative-determinant matrix is exported as a negative sx
with the angle of the mirrored axis; drawing it with a negative width put the
piece on the wrong side, upside down (the control guide's second drum stick,
and mirrored leaves in the full-combo / donderful banners).  Such a leaf is now
drawn as the |sx| copy mirrored horizontally, with the pivot moved to w - ox
inside the flipped image and the angle brought back by 180 degrees.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 01:43:57 +08:00
42 changed files with 663 additions and 1210 deletions

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

@@ -128,25 +128,5 @@
"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.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

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.3 KiB

After

Width:  |  Height:  |  Size: 2.2 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: 121 B

After

Width:  |  Height:  |  Size: 112 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: 121 B

After

Width:  |  Height:  |  Size: 112 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": 738,
"x": 726,
"y": 248
},
"2p_bar": {
"x": 738,
"x": 726,
"y": 248
},
"1p_bar_fade": {
"x": 733,
"x": 716,
"y": 241
},
"2p_bar_fade": {
"x": 733,
"x": 716,
"y": 241
},
"tamashii": {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 B

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 B

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 B

After

Width:  |  Height:  |  Size: 109 B

+10 -19
View File
@@ -272,21 +272,21 @@
"1p_bar": [
{
"x": 87,
"y": 240
"y": 241
},
{
"x": 1034,
"y": 240
"y": 241
}
],
"2p_bar": [
{
"x": 87,
"y": 240
"y": 241
},
{
"x": 1034,
"y": 240
"y": 241
}
],
"bar_clear_bottom": [
@@ -302,20 +302,20 @@
"bar_clear_top": [
{
"x": 87,
"y": 217
"y": 216
},
{
"x": 1034,
"y": 217
"y": 216
}
],
"bar_clear_transition": [
{
"x": 87,
"x": 89,
"y": 215
},
{
"x": 1034,
"x": 1036,
"y": 215
}
],
@@ -349,16 +349,7 @@
"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,
@@ -473,4 +464,4 @@
"y": 210
}
]
}
}
+26 -252
View File
@@ -1,31 +1,4 @@
{
"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,
@@ -45,8 +18,7 @@
"costume_preset_slots": true,
"dan_title_horizontal": true,
"combo_color_tiers": true,
"gauge_cell_fade_in": true,
"dan_exam_grid": true
"gauge_cell_fade_in": true
}
},
"score_counter_pos": {
@@ -67,7 +39,7 @@
},
"song_info": {
"x": 1872,
"y": 68,
"y": 58,
"font_size": 54,
"outline": 4.333333
},
@@ -193,7 +165,7 @@
"text": {
"ja": "太鼓をたたいてスタート!",
"en": "Hit the taiko to start!",
"zh": "敲打太鼓开始!"
"zh": "敲打太鼓開始!"
}
},
"crown_message_miss": {
@@ -213,7 +185,7 @@
"text": {
"ja": "おしかったドン",
"en": "Almost.",
"zh": "可惜!\n就差一点了鼓咚…"
"zh": "可惜!\n就差一點了鼓咚…"
}
},
"crown_message_success": {
@@ -223,7 +195,7 @@
"text": {
"ja": "ノリノリだドン",
"en": "Cleared!\nCongrats, Don!",
"zh": "过关\n恭喜鼓咚!"
"zh": "過關\n恭喜鼓咚!"
}
},
"crown_message_perfect": {
@@ -233,7 +205,7 @@
"text": {
"ja": "よくできたドン!",
"en": "Awesome!",
"zh": "魂槽全满!\n太厉害了鼓咚!"
"zh": "魂槽全滿!\n太厲害了鼓咚!"
}
},
"song_select_indicator": {
@@ -277,7 +249,7 @@
"text": {
"en": "Select your outfit!",
"ja": "きせかえをえらぼう!",
"zh": "选择服饰吧!"
"zh": "選擇服飾吧!"
}
},
"result_chara": {
@@ -326,7 +298,7 @@
"text": {
"ja": "かんたん",
"en": "Easy",
"zh": "简单"
"zh": "簡單"
}
},
"course_name_1": {
@@ -346,7 +318,7 @@
"text": {
"ja": "むずかしい",
"en": "Hard",
"zh": "困难"
"zh": "困難"
}
},
"course_name_3": {
@@ -376,7 +348,7 @@
"text": {
"ja": "えらぶ",
"en": "Select",
"zh": "选择"
"zh": "選擇"
}
},
"guide_decide_label": {
@@ -386,7 +358,7 @@
"text": {
"ja": "決定",
"en": "Decide",
"zh": "决定"
"zh": "決定"
}
},
"nameplate_entry_left": {
@@ -404,7 +376,7 @@
"text": {
"ja": "かんたん",
"en": "Easy",
"zh": "简单"
"zh": "簡單"
}
},
"chip_course_name_1": {
@@ -424,7 +396,7 @@
"text": {
"ja": "むずかしい",
"en": "Hard",
"zh": "困难"
"zh": "困難"
}
},
"chip_course_name_3": {
@@ -519,7 +491,7 @@
"text": {
"en": "No sound",
"ja": "無音",
"zh": "无声"
"zh": "無聲"
}
},
"modifier_title": {
@@ -529,7 +501,7 @@
"text": {
"ja": "演奏オプション",
"en": "Play Options",
"zh": "演奏选项"
"zh": "演奏選項"
}
},
"song_select_chara_1p": {
@@ -572,7 +544,7 @@
},
"gauge_cells": {
"x": 50
},
},
"mod_badge_grid": {
"x": 170,
"y": 77,
@@ -690,7 +662,7 @@
"text": {
"en": "1-Player",
"ja": "1人プレイ",
"zh": "1人游玩"
"zh": "1人遊玩"
}
},
"entry_credit_2p": {
@@ -700,7 +672,7 @@
"text": {
"en": "2-Player",
"ja": "2人プレイ",
"zh": "2人游玩"
"zh": "2人遊玩"
}
},
"entry_credit_start": {
@@ -710,7 +682,7 @@
"text": {
"en": "Hit the drum to start!",
"ja": "太鼓をたたいてスタート!",
"zh": "敲打太鼓开始游戏!"
"zh": "敲打太鼓開始遊戲!"
}
},
"entry_cancel": {
@@ -809,6 +781,7 @@
"song_num_max_game": {
"x": 1814,
"y": 122
},
"note_arc_curve_height": {
"height": 608
@@ -900,8 +873,8 @@
"height": 132
},
"dan_exam_seg": {
"x": 11,
"y": 47,
"x": 14,
"y": 48,
"width": 327
},
"dan_select_exam_border_text": {
@@ -953,45 +926,22 @@
},
"dan_exam_text_more": {
"text": {
"ja": "%s 以上",
"en": "%s or more",
"zh": "%s 以上",
"ko": "%s 이상"
"ja": "%s 以上"
}
},
"dan_exam_text_less": {
"text": {
"ja": "%s 未満",
"en": "under %s",
"zh": "%s 未满",
"ko": "%s 미만"
"ja": "%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 %以上",
"en": "%s%% or more",
"zh": "%s%以上",
"ko": "%s% 이상"
"ja": "%s %以上"
}
},
"dan_exam_text_gauge_less": {
"text": {
"ja": "%s %未満",
"en": "under %s%%",
"zh": "%s%未满",
"ko": "%s% 미만"
"ja": "%s %未満"
}
},
"note_arc_pivot": {
@@ -1001,181 +951,5 @@
"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": "段位道场"
}
}
}
+229 -235
View File
@@ -1,14 +1,12 @@
-- GENERATED from the cabinet clip with lumen_anim_dump -- DO NOT EDIT BY HAND.
-- Re-export: lumen_anim_dump.exe enso_normal/song_select/song_select_main_genre.nulm --sprite 19 --all --range 27,255
-- Source: enso_normal/song_select/song_select_main_genre.nulm clip #19 frames 27..255 @ 60 fps
-- Rows are {frame, tx, ty}; every field is LINEAR between two rows. f27..150 is the
-- folder OPEN leg (genre_deceide f45, return f150), f150..255 the CLOSE leg (return
-- f150 .. genre_deceide2 f255); at f255 the clip re-bases for a second decide.
-- GENERATED by scratchpad/anim_export.py -- DO NOT EDIT BY HAND.
-- Re-export: python anim_export.py enso_normal/song_select/song_select_main_genre.nulm #19 --range 27,150 --fields tx,ty --out wheel_decide.lua
-- Source: enso_normal/song_select/song_select_main_genre.nulm clip #19 frames 27..150 @ 60 fps
-- Rows are {frame, tx, ty}; every field is LINEAR between two rows.
return {
movie = "enso_normal/song_select/song_select_main_genre.nulm",
clip = "#19",
fps = 60,
range = { 27, 255 },
range = { 27, 150 },
fields = { "tx", "ty" },
labels = {
["init"] = 0,
@@ -23,36 +21,13 @@ return {
tracks = {
["kanban_genre_1"] = {
{ 27, 5.55, 28.35 },
{ 28, 11.1, 56.65 },
{ 30, 22.2, 113.35 },
{ 31, 27.8, 141.65 },
{ 33, 38.9, 198.35 },
{ 34, 44.45, 226.65 },
{ 35, 50, 255 },
{ 36, 44.45, 226.65 },
{ 37, 38.9, 198.35 },
{ 39, 27.8, 141.65 },
{ 40, 22.2, 113.35 },
{ 42, 11.1, 56.65 },
{ 43, 5.55, 28.35 },
{ 44, 0, 0 },
{ 254, 0, 0 },
{ 255, 50, 255 },
{ 150, 0, 0 },
},
["kanban_genre_2"] = {
{ 27, -44.45, -226.65 },
{ 28, -38.9, -198.35 },
{ 30, -27.8, -141.65 },
{ 31, -22.2, -113.35 },
{ 33, -11.1, -56.65 },
{ 34, -5.55, -28.35 },
{ 35, 0, 0 },
{ 36, -5.55, -28.35 },
{ 37, -11.1, -56.65 },
{ 39, -22.2, -113.35 },
{ 40, -27.8, -141.65 },
{ 42, -38.9, -198.35 },
{ 43, -44.45, -226.65 },
{ 44, -50, -255 },
{ 86, -50, -255 },
{ 87, -50.7, -257.4 },
@@ -82,44 +57,11 @@ return {
{ 139, -52.85, -264.6 },
{ 140, -50.7, -257.4 },
{ 141, -50, -255 },
{ 191, -50, -255 },
{ 192, -50.7, -257.4 },
{ 193, -52.85, -264.6 },
{ 194, -56.4, -276.55 },
{ 195, -61.35, -293.35 },
{ 196, -67.75, -314.9 },
{ 197, -75.55, -341.25 },
{ 198, -84.8, -372.45 },
{ 199, -95.45, -408.4 },
{ 200, -107.5, -449.1 },
{ 201, -121, -494.65 },
{ 202, -135.9, -544.95 },
{ 203, -152.25, -600.1 },
{ 204, -170, -660 },
{ 233, -170, -660 },
{ 234, -152.25, -600.1 },
{ 235, -135.9, -545 },
{ 236, -121, -494.65 },
{ 237, -107.5, -449.1 },
{ 238, -95.45, -408.4 },
{ 239, -84.8, -372.45 },
{ 240, -75.55, -341.25 },
{ 241, -67.75, -314.9 },
{ 242, -61.35, -293.35 },
{ 243, -56.4, -276.55 },
{ 244, -52.85, -264.6 },
{ 245, -50.7, -257.4 },
{ 246, -50, -255 },
{ 254, -50, -255 },
{ 255, 0, 0 },
{ 150, -50, -255 },
},
["kanban_genre_3"] = {
{ 27, 54.45, 270 },
{ 30, 67.8, 315 },
{ 31, 72.2, 330 },
{ 35, 90, 390 },
{ 39, 72.2, 330 },
{ 40, 67.8, 315 },
{ 44, 50, 255 },
{ 86, 50, 255 },
{ 87, 50.7, 257.4 },
@@ -149,44 +91,11 @@ return {
{ 139, 52.85, 264.6 },
{ 140, 50.7, 257.4 },
{ 141, 50, 255 },
{ 191, 50, 255 },
{ 192, 50.7, 257.4 },
{ 193, 52.85, 264.6 },
{ 194, 56.4, 276.55 },
{ 195, 61.35, 293.35 },
{ 196, 67.75, 314.9 },
{ 197, 75.55, 341.25 },
{ 198, 84.8, 372.45 },
{ 199, 95.45, 408.4 },
{ 200, 107.5, 449.1 },
{ 201, 121, 494.65 },
{ 202, 135.9, 544.95 },
{ 203, 152.25, 600.1 },
{ 204, 170, 660 },
{ 233, 170, 660 },
{ 234, 152.25, 600.1 },
{ 235, 135.9, 545 },
{ 236, 121, 494.65 },
{ 237, 107.5, 449.1 },
{ 238, 95.45, 408.4 },
{ 239, 84.8, 372.45 },
{ 240, 75.55, 341.25 },
{ 241, 67.75, 314.9 },
{ 242, 61.35, 293.35 },
{ 243, 56.4, 276.55 },
{ 244, 52.85, 264.6 },
{ 245, 50.7, 257.4 },
{ 246, 50, 255 },
{ 254, 50, 255 },
{ 255, 90, 390 },
{ 150, 50, 255 },
},
["kanban_genre_4"] = {
{ 27, -85.55, -375 },
{ 30, -72.2, -330 },
{ 31, -67.8, -315 },
{ 35, -50, -255 },
{ 39, -67.8, -315 },
{ 40, -72.2, -330 },
{ 44, -90, -390 },
{ 82, -90, -390 },
{ 83, -90.7, -392.4 },
@@ -216,44 +125,11 @@ return {
{ 143, -92.85, -399.6 },
{ 144, -90.7, -392.4 },
{ 145, -90, -390 },
{ 187, -90, -390 },
{ 188, -90.7, -392.4 },
{ 189, -92.85, -399.6 },
{ 190, -96.4, -411.55 },
{ 191, -101.35, -428.35 },
{ 192, -107.75, -449.9 },
{ 193, -115.55, -476.25 },
{ 194, -124.8, -507.45 },
{ 195, -135.45, -543.4 },
{ 196, -147.5, -584.1 },
{ 197, -161, -629.65 },
{ 198, -175.9, -679.95 },
{ 199, -192.25, -735.1 },
{ 200, -210, -795 },
{ 237, -210, -795 },
{ 238, -192.25, -735.1 },
{ 239, -175.9, -680 },
{ 240, -161, -629.65 },
{ 241, -147.5, -584.1 },
{ 242, -135.45, -543.4 },
{ 243, -124.8, -507.45 },
{ 244, -115.55, -476.25 },
{ 245, -107.75, -449.9 },
{ 246, -101.35, -428.35 },
{ 247, -96.4, -411.55 },
{ 248, -92.85, -399.6 },
{ 249, -90.7, -392.4 },
{ 250, -90, -390 },
{ 254, -90, -390 },
{ 255, -50, -255 },
{ 150, -90, -390 },
},
["kanban_genre_5"] = {
{ 27, 94.45, 405 },
{ 30, 107.8, 450 },
{ 31, 112.2, 465 },
{ 35, 130, 525 },
{ 39, 112.2, 465 },
{ 40, 107.8, 450 },
{ 44, 90, 390 },
{ 82, 90, 390 },
{ 83, 90.7, 392.4 },
@@ -283,44 +159,11 @@ return {
{ 143, 92.85, 399.6 },
{ 144, 90.7, 392.4 },
{ 145, 90, 390 },
{ 187, 90, 390 },
{ 188, 90.7, 392.4 },
{ 189, 92.85, 399.6 },
{ 190, 96.4, 411.55 },
{ 191, 101.35, 428.35 },
{ 192, 107.75, 449.9 },
{ 193, 115.55, 476.25 },
{ 194, 124.8, 507.45 },
{ 195, 135.45, 543.4 },
{ 196, 147.5, 584.1 },
{ 197, 161, 629.65 },
{ 198, 175.9, 679.95 },
{ 199, 192.25, 735.1 },
{ 200, 210, 795 },
{ 237, 210, 795 },
{ 238, 192.25, 735.1 },
{ 239, 175.9, 680 },
{ 240, 161, 629.65 },
{ 241, 147.5, 584.1 },
{ 242, 135.45, 543.4 },
{ 243, 124.8, 507.45 },
{ 244, 115.55, 476.25 },
{ 245, 107.75, 449.9 },
{ 246, 101.35, 428.35 },
{ 247, 96.4, 411.55 },
{ 248, 92.85, 399.6 },
{ 249, 90.7, 392.4 },
{ 250, 90, 390 },
{ 254, 90, 390 },
{ 255, 130, 525 },
{ 150, 90, 390 },
},
["kanban_genre_6"] = {
{ 27, -125.55, -510 },
{ 30, -112.2, -465 },
{ 31, -107.8, -450 },
{ 35, -90, -390 },
{ 39, -107.8, -450 },
{ 40, -112.2, -465 },
{ 44, -130, -525 },
{ 78, -130, -525 },
{ 79, -130.7, -527.4 },
@@ -350,43 +193,11 @@ return {
{ 147, -132.85, -534.6 },
{ 148, -130.7, -527.4 },
{ 149, -130, -525 },
{ 183, -130, -525 },
{ 184, -130.7, -527.4 },
{ 185, -132.85, -534.6 },
{ 186, -136.4, -546.55 },
{ 187, -141.35, -563.35 },
{ 188, -147.75, -584.9 },
{ 189, -155.55, -611.25 },
{ 190, -164.8, -642.45 },
{ 191, -175.45, -678.4 },
{ 192, -187.5, -719.1 },
{ 193, -201, -764.65 },
{ 194, -215.9, -814.95 },
{ 195, -232.25, -870.1 },
{ 196, -250, -930 },
{ 241, -250, -930 },
{ 242, -232.25, -870.1 },
{ 243, -215.9, -815 },
{ 244, -201, -764.65 },
{ 245, -187.5, -719.1 },
{ 246, -175.45, -678.4 },
{ 247, -164.8, -642.45 },
{ 248, -155.55, -611.25 },
{ 249, -147.75, -584.9 },
{ 250, -141.35, -563.35 },
{ 251, -136.4, -546.55 },
{ 252, -132.85, -534.6 },
{ 253, -130.7, -527.4 },
{ 254, -130, -525 },
{ 255, -90, -390 },
{ 150, -130, -525 },
},
["kanban_genre_7"] = {
{ 27, 134.45, 540 },
{ 30, 147.8, 585 },
{ 31, 152.2, 600 },
{ 35, 170, 660 },
{ 39, 152.2, 600 },
{ 40, 147.8, 585 },
{ 44, 130, 525 },
{ 78, 130, 525 },
{ 79, 130.7, 527.4 },
@@ -416,46 +227,229 @@ return {
{ 147, 132.85, 534.6 },
{ 148, 130.7, 527.4 },
{ 149, 130, 525 },
{ 183, 130, 525 },
{ 184, 130.7, 527.4 },
{ 185, 132.85, 534.6 },
{ 186, 136.4, 546.55 },
{ 187, 141.35, 563.35 },
{ 188, 147.75, 584.9 },
{ 189, 155.55, 611.25 },
{ 190, 164.8, 642.45 },
{ 191, 175.45, 678.4 },
{ 192, 187.5, 719.1 },
{ 193, 201, 764.65 },
{ 194, 215.9, 814.95 },
{ 195, 232.25, 870.1 },
{ 196, 250, 930 },
{ 241, 250, 930 },
{ 242, 232.25, 870.1 },
{ 243, 215.9, 815 },
{ 244, 201, 764.65 },
{ 245, 187.5, 719.1 },
{ 246, 175.45, 678.4 },
{ 247, 164.8, 642.45 },
{ 248, 155.55, 611.25 },
{ 249, 147.75, 584.9 },
{ 250, 141.35, 563.35 },
{ 251, 136.4, 546.55 },
{ 252, 132.85, 534.6 },
{ 253, 130.7, 527.4 },
{ 254, 130, 525 },
{ 255, 170, 660 },
{ 150, 130, 525 },
},
["kanban_genre_8"] = {
{ 27, -165.55, -645 },
{ 30, -152.2, -600 },
{ 31, -147.8, -585 },
{ 35, -130, -525 },
{ 39, -147.8, -585 },
{ 40, -152.2, -600 },
{ 44, -170, -660 },
{ 254, -170, -660 },
{ 255, -130, -525 },
{ 150, -170, -660 },
},
["kanban_song_1"] = {
{ 27, 5.55, 28.35 },
{ 35, 50, 255 },
{ 44, 0, 0 },
{ 150, 0, 0 },
},
["kanban_song_2"] = {
{ 27, -44.45, -226.65 },
{ 35, 0, 0 },
{ 44, -50, -255 },
{ 86, -50, -255 },
{ 87, -50.7, -257.4 },
{ 88, -52.85, -264.6 },
{ 89, -56.4, -276.55 },
{ 90, -61.35, -293.35 },
{ 91, -67.75, -314.9 },
{ 92, -75.55, -341.25 },
{ 93, -84.8, -372.45 },
{ 94, -95.45, -408.4 },
{ 95, -107.5, -449.1 },
{ 96, -121, -494.65 },
{ 97, -135.9, -544.95 },
{ 98, -152.25, -600.1 },
{ 99, -170, -660 },
{ 128, -170, -660 },
{ 129, -152.25, -600.1 },
{ 130, -135.9, -545 },
{ 131, -121, -494.65 },
{ 132, -107.5, -449.1 },
{ 133, -95.45, -408.4 },
{ 134, -84.8, -372.45 },
{ 135, -75.55, -341.25 },
{ 136, -67.75, -314.9 },
{ 137, -61.35, -293.35 },
{ 138, -56.4, -276.55 },
{ 139, -52.85, -264.6 },
{ 140, -50.7, -257.4 },
{ 141, -50, -255 },
{ 150, -50, -255 },
},
["kanban_song_3"] = {
{ 27, 54.45, 270 },
{ 35, 90, 390 },
{ 44, 50, 255 },
{ 86, 50, 255 },
{ 87, 50.7, 257.4 },
{ 88, 52.85, 264.6 },
{ 89, 56.4, 276.55 },
{ 90, 61.35, 293.35 },
{ 91, 67.75, 314.9 },
{ 92, 75.55, 341.25 },
{ 93, 84.8, 372.45 },
{ 94, 95.45, 408.4 },
{ 95, 107.5, 449.1 },
{ 96, 121, 494.65 },
{ 97, 135.9, 544.95 },
{ 98, 152.25, 600.1 },
{ 99, 170, 660 },
{ 128, 170, 660 },
{ 129, 152.25, 600.1 },
{ 130, 135.9, 545 },
{ 131, 121, 494.65 },
{ 132, 107.5, 449.1 },
{ 133, 95.45, 408.4 },
{ 134, 84.8, 372.45 },
{ 135, 75.55, 341.25 },
{ 136, 67.75, 314.9 },
{ 137, 61.35, 293.35 },
{ 138, 56.4, 276.55 },
{ 139, 52.85, 264.6 },
{ 140, 50.7, 257.4 },
{ 141, 50, 255 },
{ 150, 50, 255 },
},
["kanban_song_4"] = {
{ 27, -85.55, -375 },
{ 35, -50, -255 },
{ 44, -90, -390 },
{ 82, -90, -390 },
{ 83, -90.7, -392.4 },
{ 84, -92.85, -399.6 },
{ 85, -96.4, -411.55 },
{ 86, -101.35, -428.35 },
{ 87, -107.75, -449.9 },
{ 88, -115.55, -476.25 },
{ 89, -124.8, -507.45 },
{ 90, -135.45, -543.4 },
{ 91, -147.5, -584.1 },
{ 92, -161, -629.65 },
{ 93, -175.9, -679.95 },
{ 94, -192.25, -735.1 },
{ 95, -210, -795 },
{ 132, -210, -795 },
{ 133, -192.25, -735.1 },
{ 134, -175.9, -680 },
{ 135, -161, -629.65 },
{ 136, -147.5, -584.1 },
{ 137, -135.45, -543.4 },
{ 138, -124.8, -507.45 },
{ 139, -115.55, -476.25 },
{ 140, -107.75, -449.9 },
{ 141, -101.35, -428.35 },
{ 142, -96.4, -411.55 },
{ 143, -92.85, -399.6 },
{ 144, -90.7, -392.4 },
{ 145, -90, -390 },
{ 150, -90, -390 },
},
["kanban_song_5"] = {
{ 27, 94.45, 405 },
{ 35, 130, 525 },
{ 44, 90, 390 },
{ 82, 90, 390 },
{ 83, 90.7, 392.4 },
{ 84, 92.85, 399.6 },
{ 85, 96.4, 411.55 },
{ 86, 101.35, 428.35 },
{ 87, 107.75, 449.9 },
{ 88, 115.55, 476.25 },
{ 89, 124.8, 507.45 },
{ 90, 135.45, 543.4 },
{ 91, 147.5, 584.1 },
{ 92, 161, 629.65 },
{ 93, 175.9, 679.95 },
{ 94, 192.25, 735.1 },
{ 95, 210, 795 },
{ 132, 210, 795 },
{ 133, 192.25, 735.1 },
{ 134, 175.9, 680 },
{ 135, 161, 629.65 },
{ 136, 147.5, 584.1 },
{ 137, 135.45, 543.4 },
{ 138, 124.8, 507.45 },
{ 139, 115.55, 476.25 },
{ 140, 107.75, 449.9 },
{ 141, 101.35, 428.35 },
{ 142, 96.4, 411.55 },
{ 143, 92.85, 399.6 },
{ 144, 90.7, 392.4 },
{ 145, 90, 390 },
{ 150, 90, 390 },
},
["kanban_song_6"] = {
{ 27, -125.55, -510 },
{ 35, -90, -390 },
{ 44, -130, -525 },
{ 78, -130, -525 },
{ 79, -130.7, -527.4 },
{ 80, -132.85, -534.6 },
{ 81, -136.4, -546.55 },
{ 82, -141.35, -563.35 },
{ 83, -147.75, -584.9 },
{ 84, -155.55, -611.25 },
{ 85, -164.8, -642.45 },
{ 86, -175.45, -678.4 },
{ 87, -187.5, -719.1 },
{ 88, -201, -764.65 },
{ 89, -215.9, -814.95 },
{ 90, -232.25, -870.1 },
{ 91, -250, -930 },
{ 136, -250, -930 },
{ 137, -232.25, -870.1 },
{ 138, -215.9, -815 },
{ 139, -201, -764.65 },
{ 140, -187.5, -719.1 },
{ 141, -175.45, -678.4 },
{ 142, -164.8, -642.45 },
{ 143, -155.55, -611.25 },
{ 144, -147.75, -584.9 },
{ 145, -141.35, -563.35 },
{ 146, -136.4, -546.55 },
{ 147, -132.85, -534.6 },
{ 148, -130.7, -527.4 },
{ 149, -130, -525 },
{ 150, -130, -525 },
},
["kanban_song_7"] = {
{ 27, 134.45, 540 },
{ 35, 170, 660 },
{ 44, 130, 525 },
{ 78, 130, 525 },
{ 79, 130.7, 527.4 },
{ 80, 132.85, 534.6 },
{ 81, 136.4, 546.55 },
{ 82, 141.35, 563.35 },
{ 83, 147.75, 584.9 },
{ 84, 155.55, 611.25 },
{ 85, 164.8, 642.45 },
{ 86, 175.45, 678.4 },
{ 87, 187.5, 719.1 },
{ 88, 201, 764.65 },
{ 89, 215.9, 814.95 },
{ 90, 232.25, 870.1 },
{ 91, 250, 930 },
{ 136, 250, 930 },
{ 137, 232.25, 870.1 },
{ 138, 215.9, 815 },
{ 139, 201, 764.65 },
{ 140, 187.5, 719.1 },
{ 141, 175.45, 678.4 },
{ 142, 164.8, 642.45 },
{ 143, 155.55, 611.25 },
{ 144, 147.75, 584.9 },
{ 145, 141.35, 563.35 },
{ 146, 136.4, 546.55 },
{ 147, 132.85, 534.6 },
{ 148, 130.7, 527.4 },
{ 149, 130, 525 },
{ 150, 130, 525 },
},
["kanban_song_8"] = {
{ 27, -165.55, -645 },
{ 35, -130, -525 },
{ 44, -170, -660 },
{ 150, -170, -660 },
},
},
}
+2 -5
View File
@@ -251,13 +251,10 @@ function M:draw()
local x = GAUGE_X0 + CELL_PITCH * n - CELL_PITCH / 2 - 1
if self.label_for ~= ex.red then
-- 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,
self.label = text.create_raw_text(tostring(ex.red) .. "%以上",
VALUE_SIZE, CAP_FILL, VALUE_OUTLINE, false,
4.0 / tex.get_screen_scale())
self.name_label = text.create_raw_text(tex.skin_text("dan_gauge_name"),
self.name_label = text.create_raw_text("魂ゲージ",
NAME_SIZE, CAP_FILL, CAP_OUTLINE, false,
3.0 / tex.get_screen_scale())
self.label_for = ex.red
+6 -3
View File
@@ -75,14 +75,17 @@ function Ending:draw(current_ms)
local s = shapes[r[1]]
local id = s and self:id_of(r[1])
if id then
local sx, sy = r[4], r[5]
local sx, sy, rot, ox, mirror = r[4], r[5], r[6], s[3], nil
-- mirrored leaf: see game/clip_player.lua
if sx < 0 then sx, rot, ox, mirror = -sx, rot - 180, s[1] - s[3], "horizontal" end
tex.draw_texture(id, {
x = CLIP_X + r[2],
y = self.y + CLIP_Y + r[3],
x2 = s[1] * (sx - 1),
y2 = s[2] * (sy - 1),
origin = { s[3] * sx, s[4] * sy },
rotation = r[6],
origin = { ox * sx, s[4] * sy },
rotation = rot,
mirror = mirror,
fade = r[7],
blend = (r[8] == 8) and "additive" or nil, -- cabinet add-blended leaf
})
+41 -28
View File
@@ -28,11 +28,7 @@ local INFO_SIZE = 34
-- text_title_instance sits at y=-106 (on) / 0 (off); inside it text_instance_1/2 at -84.5 hold the
-- 758x126 text box at (-378,2) -> box centre -20.5 (off) / -126.5 (on). Lumen dynamic text is
-- vertically centred in its box (the reference research notes 3a).
-- TITLE_Y_ON: the open board's title sits in the middle of the dark band at the top of the
-- plate (plate rows 64..187 -> centre 104.5 above the board centre). The text object's
-- box is taller than its ink (descender room), so its ink centre lands ~16 px above the
-- nominal y; -97 nominal puts the ink on the band centre (tuned on screen).
local TITLE_Y_ON, TITLE_Y_OFF = -97, 4 -- closed board: user saw the title riding the top edge (+16)
local TITLE_Y_ON, TITLE_Y_OFF = -118, 4 -- closed board: user saw the title riding the top edge (+16) -- +8 px: user asked the title slightly lower
-- text_info_instance at y=57, text_info rect (-2,-2,478,192) at (-238,-92.5) -> box -37.5..156.5,
-- centre 59.5. The cabinet renderer centres the text block in the box and advances lines by the
-- atlas line height (jp_32 fontSize 53 x scale 32/32 = 53 px) -> line centres ~33 / ~86.
@@ -60,23 +56,39 @@ local TITLE_SPACING, INFO_SPACING = 2.0, 1.0
local MODES = {
game = {
frame = 0, outline = {251, 1, 29, 255},
title_key = "entry_game",
comment_keys = { "entry_game_comment_1", "entry_game_comment_2" },
titles = { ja = "演奏ゲーム", en = "Game", zh = "节奏游戏" },
comment = {
ja = { "すきな曲や、むずかしさを", "えらんであそべるよ!" },
en = { "Select a song and", "difficulty to play!" },
zh = { "可選擇喜愛的", "歌曲、難度遊玩哦!" },
},
},
practice = {
frame = 2, outline = {34, 138, 117, 255},
title_key = "entry_practice",
comment_keys = { "entry_practice_comment_1", "entry_practice_comment_2" },
titles = { ja = "特訓モード", en = "Practice", zh = "练习游戏" },
comment = {
ja = { "曲の一部をくりかえし練習して", "腕をみがこう!" },
en = { "Practice a section of a song", "over and over to improve!" },
zh = { "反覆練習歌曲的片段", "磨練技巧吧!" },
},
},
costume = {
frame = 4, outline = {200, 120, 222, 255},
title_key = "entry_costume",
comment_keys = { "entry_costume_comment_1", "entry_costume_comment_2" },
titles = { ja = "きせかえ", en = "Costume", zh = "装扮" },
comment = {
ja = { "どんちゃんのきせかえを", "えらんであそべるよ!" },
en = { "Choose an outfit", "for your Don-chan!" },
zh = { "可為咚醬選擇", "服飾遊玩哦!" },
},
},
ai = {
frame = 2, outline = {34, 138, 117, 255},
title_key = "entry_ai_battle",
comment_keys = { "entry_ai_battle_comment_1", "entry_ai_battle_comment_2" },
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咚」來一場", "不到最後難分勝負的對戰吧!" },
},
},
-- ROUND 83 (r83-dandojo-as-mode) — 段位道場 is a MODE on the cabinet
-- (`mode_select.lua` CreateBoardList l.289-296 appends GameMode.kDani right after
@@ -87,16 +99,24 @@ local MODES = {
-- mode_select_comment_dani (three lines; every other board here has two).
dan = {
frame = 6, outline = {76, 60, 151, 255},
title_key = "mode_dan",
comment_keys = { "mode_dan_comment_1", "mode_dan_comment_2", "mode_dan_comment_3" },
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首指定曲", "並達成題目的合格條件吧!", "只要合格實力就會備受肯定哦!" },
},
},
-- 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},
title_key = "entry_settings",
comment_keys = { "entry_settings_comment_1", "entry_settings_comment_2" },
titles = { ja = "ゲーム設定", en = "Settings", zh = "游戏設定" },
comment = {
ja = { "ゲームのせっていを", "かえられるよ!" },
en = { "Change the game's", "settings here!" },
zh = { "可以變更", "遊戲設定哦!" },
},
},
}
@@ -105,19 +125,16 @@ 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 tex.skin_text(m.title_key, cur) == text_str then return m, cur end
if m.titles[cur] == text_str then return m, cur end
end
end
for key, m in pairs(MODES) do
for _, lang in ipairs(LANGS) do
if tex.skin_text(m.title_key, lang) == text_str then return m, lang end
for lang, t in pairs(m.titles) do
if t == text_str then return m, lang end
end
end
return MODES.settings, "ja", true
@@ -276,8 +293,7 @@ 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 = {}
for i, key in ipairs(mode.comment_keys) do lines[i] = tex.skin_text(key, lang) end
local lines = mode.comment[lang] or mode.comment.ja
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)
@@ -332,9 +348,6 @@ function EntryBox:draw(x, left_x, right_x, is_selected, move_finished, open_attr
local now = tex.get_current_ms()
local shared = __hss_entry_mode
if shared.in_t0 == nil then shared.in_t0 = now end -- first frame the list is drawn
-- ModeSelect: the cabinet's guide shows `all` (rim to choose + face to decide) when the
-- list has more than one board, `decide` alone otherwise (entry_main.lua ChangeMode)
__hss_entry_guide = { ms = now, phase = "mode", count = #__hss_entry_boxes }
if is_selected and shared.sel ~= self.index then
shared.sel = self.index
shared.t0 = now
+19 -3
View File
@@ -86,6 +86,21 @@ 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
@@ -125,9 +140,10 @@ 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)
-- 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)
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)
self.costume_select_mode = false
-- arcade cursor_idx_ (1..5, here 0-based) and select_offset_
+231 -11
View File
@@ -1,8 +1,12 @@
-- YataiDON-HSS override of PyTaikoGreen/entry/entry.lua: cancel text centring uses cancel_info.x2[1]/y2[1];
-- sets __hss_entry_reset for the arcade mode-select list in box.lua
Entry = {}
Entry.__index = Entry
function Entry.new()
local self = setmetatable({}, Entry)
-- EntryScreen::on_screen_start creates BoxManager (4 EntryBox) then this script;
-- flag the box list so the NEXT batch of EntryBox.new starts fresh (see box.lua).
__hss_entry_reset = true
tex.load_animations("entry")
@@ -10,11 +14,36 @@ function Entry.new()
self.side_select_fade = tex.get_animation(0, "entry")
self.bg_flicker:start()
-- ROUND 11 -- the arcade "credit wait" screen (lumen entry/entry_credit) has no
-- 「どちらでつかいますか?」 box: two pills carrying 「1人プレイ」/「2人プレイ」 on the pale
-- left half and 「太鼓をたたいてスタート!」 on the yellow right half. Strings are the
-- cabinet's own wordlist rows credit_play_1p / credit_play_2p / credit_start.
-- Outline thickness is scaled by screen_scale (1.5) in OutlinedText's ctor, so the
-- arcade's DefineEditText border 7 is passed as 5 (= 7.5 px).
-- ROUND 11b: 「つかわない」 is drawn as a small CAPTION, only while side 1 is armed.
-- The arcade credit screen has no cancel widget at all (entry_main.lua leaves the screen
-- on the 60 s timer, never on a button), but the engine's side 1 is real and reachable.
self.text_cancel = text.create_text("entry_cancel", { 255, 255, 255, 255 }, { 40, 24, 12, 255 }, false, 3, 0)
self.text_credit_p = {
-- ROUND 15 -- outline thickness 0, NOT the record's border 7. ET7 is `#000000` with
-- border 7, and the cabinet's outline colour is the renderer default (black), so the
-- border on this field is a pure 7.5-screen-px BLACK-ON-BLACK dilation: it carries no
-- colour information at all, it only fattens the glyphs. On 「1人プレイ」 that is
-- invisible (kana strokes are far apart), but at font 56 it closes every counter of a
-- Latin string and「1-Player」/「2-Player」 render as an unreadable black smear
-- (`scratchpad/r15ae/z_1player_en.png`). Deliberate deviation, documented here: the
-- fill colour is unchanged, so the label still reads exactly as the cabinet's black
-- text, and it is legible in every language this skin ships.
-- black ink with the cabinet's WHITE outline (same 7 stage-px border as the message)
text.create_text("entry_credit_1p", { 0, 0, 0, 255 }, { 255, 255, 255, 255 }, false, 7.0 / tex.get_screen_scale(), 0),
text.create_text("entry_credit_2p", { 0, 0, 0, 255 }, { 255, 255, 255, 255 }, false, 7.0 / tex.get_screen_scale(), 0),
}
-- the message is ONE string drawn in two colourways: the arcade stacks a black-tinted
-- outline copy (ET3 via cmul (0,0,0)) under a white fill copy (ET5), and its blink
-- recolours that outline to (256,236,67). Cross-fading these two reproduces it.
-- ROUND 16 (r16-outline): `entry/entry_credit` ET3 `text_credit` carries border 7.0,
-- which is already in 1080p stage px; OutlinedText multiplies by screen_scale (1.5),
-- so it is divided out rather than rounded to 5 (which drew 7.5 px).
local CREDIT_BORDER = 7.0 / tex.get_screen_scale()
self.text_credit_msg = text.create_text("entry_credit_start", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, CREDIT_BORDER, 0)
self.text_credit_msg_hl = text.create_text("entry_credit_start", { 255, 255, 255, 255 }, { 255, 236, 67, 255 }, false, CREDIT_BORDER, 0)
@@ -27,11 +56,19 @@ function Entry.new()
self.bg_shops_right = tex.load_texture("entry/background/shops_right")
self.bg_lights = tex.load_texture("entry/background/lights")
-- ROUND 6 — layered arcade backdrop (lumen entry/entry, entrybg_instance MC 19).
-- Arcade display list: d0 shape1 (gold sky) / d1 MC11 twinkles / d2 shape12 (street, sky
-- transparent) / d3-6 MC16 glow x4 (blend=add) / d7 shape17 (fully lit street, opaque).
-- `bg` is byte-exact shape1+shape12, so only d3-d7 have to be re-added.
self.bg_street_lit = tex.load_texture("entry/background/street_lit")
self.bg_glow = tex.load_texture("entry/background/glow")
self.bg_twinkle = tex.load_texture("entry/background/twinkle")
self.bg_t0 = tex.get_current_ms()
-- ROUND 11: the parent's nine-slice box / 1P / 2P / cancel / footer art is no longer
-- drawn (the arcade credit screen has none of it). These four are the arcade's own
-- shapes, baked by Graphics/entry/bake_credit.py; both pills are the SAME 1160x200
-- plate placed twice (texture.json carries the two placements).
self.cr_pill = tex.load_texture("entry/side_select/credit_pill")
self.cr_flash = tex.load_texture("entry/side_select/credit_flash")
self.cr_t0 = tex.get_current_ms()
@@ -41,12 +78,42 @@ function Entry.new()
self.cr_seen_frame = nil
self.cr_decide_t0 = nil
self.cr_decide_row = nil
-- ROUND 12 -- opt-in arcade CREDIT WAIT interaction model. skin_config_entry.json's
-- `entry_credit_arcade` = 1 switches the engine (EntryScreen::handle_input) to the
-- cabinet's rule: the two rows are PROMPTS, both lit, no cursor, no 「つかわない」;
-- whichever SEAT hits its own don enters as that player. PyTaikoGreen leaves the key
-- out and keeps the kat-moved side cursor.
local arcade_cfg = tex.get_skin_config("entry_credit_arcade")
self.cr_arcade = (arcade_cfg ~= nil and (arcade_cfg.x or 0) >= 1)
self.cr_pending_decide = nil
self.global_player_entry = tex.load_texture("entry/global/player_entry")
-- ROUND 18 (r18-misc) -- the top-left control guide belongs on the CREDIT WAIT
-- screen and we were not drawing it there at all.
--
-- The cabinet's own source settles it. `entry.nulm` root (sprite 54) carries
-- `controlguide_instance` at depth 28, i.e. a sibling of `entrycredit_instance`
-- (depth 23) and drawn OVER it. `script_lua/entry/entry_main.lua` CreditWait,
-- lines 957-966:
-- if this.start_guide_ == false then
-- if PlayDataManager.GameCost1 <= PlayDataManager.GetNowCoin()
-- or PlayDataManager.IsFreePlay() == true then
-- this.control_guide_:SetVisible(true)
-- this.control_guide_:Start(1)
-- and `common/ControlGuide.lua` `Start(mode)` -> `ChangeMode(1)` ->
-- `taiko_small:GotoAndStop("decide")`. So on a free-play cabinet the guide is up
-- from the first frame of the credit screen, in its **decide** state -- exactly the
-- second half of the loop `global/indicator/background` already bakes (arcade frames
-- 420..649; see Scripts/global/indicator.lua). It is torn down again at
-- ModeSelectFinishActionWait (l.1233) and at each TaikoSelect start (l.481, l.797).
--
-- Engine-side, `Indicator` is owned by `EntryPlayer`, so it only ever draws once a
-- seat has ENTERED; before that `players` is empty and nothing draws the panel.
-- Drawing it from here is the whole fix -- same texture, same (0,0), same frames.
--
-- NOT ported: `ControlGuide.Update` slides the panel to screen centre after 3 s with
-- no input (`to_center`) and back on the next hit (`to_default`). The baked loop has
-- no centre variant, so the guide stays parked top-left.
self.guide = tex.load_texture("global/indicator/background")
self.guide_t0 = self.cr_t0
self.any_joined = false
@@ -54,12 +121,23 @@ function Entry.new()
return self
end
-- Same split as Scripts/global/indicator.lua: the bake is two guides back to back at
-- one cell per 2 arcade frames, and cells GUIDE_SPLIT..GUIDE_END-1 are the 「決定」 half.
local GUIDE_STEP = 2
local GUIDE_SPLIT = math.floor(420 / GUIDE_STEP)
local GUIDE_END = math.floor(650 / GUIDE_STEP)
-- NOTE: do NOT gate this on `tex.get_texture_info("indicator","background").frame_count`.
-- The bake is a `crop` list in texture.json, not a frame-count sheet, and
-- `get_texture_info` only reports a real count for `FramedTexture` -- for this one it
-- returns 1, so a `frame_count >= 325` guard silently drew nothing. `indicator.lua`
-- reads the same value into `self.frame_count` and never uses it, which is why the
-- discrepancy had not been noticed. `draw_texture{frame=N}` indexes the crop list fine.
function Entry:draw_control_guide()
if not self.guide then return end
-- One frame of overlap with EntryPlayer's own Indicator on the join frame is
-- possible (draw_footer publishes the join flags after this runs); both draws are
-- the same cell at the same place, so it is invisible.
if self.any_joined then return end
local n = GUIDE_END - GUIDE_SPLIT
local f = GUIDE_SPLIT + math.floor((tex.get_current_ms() - self.guide_t0) * 0.06 / GUIDE_STEP) % n
@@ -69,6 +147,10 @@ end
function Entry:update(current_ms)
self.bg_flicker:update(current_ms)
self.side_select_fade:update(current_ms)
-- ROUND 11: a frame counter is the only way Lua can see that the engine has LEFT
-- EntryState::SELECT_SIDE (draw_side_select_buttons simply stops being called), which
-- is what triggers the arcade's `choose` flash + fade_out. update() runs once per
-- frame, before draw().
self.cr_frame = (self.cr_frame or 0) + 1
end
@@ -82,6 +164,8 @@ function Entry:restart_side_select()
self:restart_credit()
end
-- credit_instance `fade_in` (sprite 11 f5..f15) restarts, and so does the row's attract
-- loop; any pending decide overlay is dropped (the 2P join re-enters SELECT_SIDE).
function Entry:restart_credit()
local now = tex.get_current_ms()
self.cr_t0 = now
@@ -97,8 +181,28 @@ function Entry:get_side_select_fade()
return self.side_select_fade.attribute
end
-- ROUND 8 -- the whole `entrybg_instance` rig (lumen `entry/entry.nulm` MC 19) is now
-- SAMPLED out of `Scripts/anim/entry_bg.lua` instead of the closed forms round 6 fitted
-- to it. See ANIM_PIPELINE.md. Tracks, in the cabinet's own display order:
-- d1 twinkles `#11@1/#5@0/#3@0` (f0), `#11@1/#8@1/#6@0` (f90),
-- `#11@1/#9@2/#3@0` (f120), `#11@1/#10@3/#6@0` (f219)
-- d3-d6 glows `#16@3/#14@0` (all four are the same 120-frame curve, in phase)
-- d7 lit street `#17@7`
-- The table covers a whole 360-frame (6 s) cycle, so every channel is just read at
-- `f % 360` -- no per-channel modulo bookkeeping.
--
-- What the export changed: the two SMALL twinkles were drawn 25 % too small at the end of
-- their burst. Round 6 modelled every burst as one envelope times a per-burst multiplier
-- (0.5 for the small pair), which gives 0.25 -> 0.5 -> 0.75. The cabinet runs them
-- 0.25 -> 0.5 -> 1.0: the multiplier applies to the GROW phase only, and the expand phase
-- adds the same absolute +0.5 to big and small alike. Alpha, the big bursts, the glow
-- triangle and the street blink were all confirmed exact.
-- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL
-- clip when a generated table is missing or malformed, so every reader below falls
-- through to its `or <constant>` default instead of taking the screen down.
local _ea_ok, entry_anim = pcall(require, "anim/sampler")
if not _ea_ok then entry_anim = { safe = function()
-- sampler.lua itself is missing: hand back the same NULL clip shape it would
local n = {}
n.at = function() return nil end; n.get = function() return nil end
n.label = function() return nil end; n.has = function() return false end
@@ -106,22 +210,48 @@ if not _ea_ok then entry_anim = { safe = function()
return n
end } end
local EBG = entry_anim.safe("anim/entry_bg")
-- ROUND 11 -- entry/entry_credit.nulm, exported by scratchpad/anim_export.py:
-- CR = sprite 10 (one player row): labels init 0 / start 5 / choose 125. Tracks
-- `text_message_instance` (the white fill copy), `text_message_instance_2` (the
-- outline copy -- fields cr,cg,cb carry its black<->yellow recolour) and `#8@4`
-- (the white decide flash, shape 8, only placed from f125).
-- CF = sprite 11 (credit_instance): fade_in f5..f15, fade_out f16..f35.
local CR = entry_anim.safe("anim/credit_row")
local CF = entry_anim.safe("anim/credit_fade")
-- Layout, straight out of the lumen (stage 1920x1080; the root places credit_instance at
-- (960,432) and the row sprite is placed twice, at ty +72 and +248):
local CREDIT_CX = 960
local ROW_CY = { 432 + 72, 432 + 248 }
local LABEL_INK_X = CREDIT_CX - 443
local MSG_INK_X = CREDIT_CX + 190
local TEXT_INK_DY = 0.5
local PAD5 = 9
local ROW_CY = { 432 + 72, 432 + 248 } -- 504 / 680
-- DefineEditText records (payload map: [2] text sym, [4] colour, [5] rect, byte 32 align,
-- f32@44 size, f32@60 outline -- Graphics/result/MAPPING.md ROUND 6):
-- ET7 text_player rect (-2,-2,306,94) placed at (-448,-62.5), align 0 (LEFT), size 56, border 7
-- ET3 message rect (-2,-2,638,94) placed at (-137,0)+(2,-62.5), align 2 (CENTRE), size 56, border 7
-- The cabinet text rule is pen.y = box.top + (box.h - lineHeight)/2 + border, so the INK
-- CENTRE is box.top + box.h/2 + border -- independent of which font raster is used:
local LABEL_INK_X = CREDIT_CX - 443 -- box.left(-450) + border(7), left-aligned
local MSG_INK_X = CREDIT_CX + 190 -- box.left(-137) + border(7) + box.w/2(320)
local TEXT_INK_DY = 0.5 -- box.top(-64.5) + box.h/2(48) + border(7), +10: measured
-- against the cabinet the ink sat ~10 px above the pill centre
local PAD5 = 9 -- OutlinedText pad for thickness 5: (int)(5*1.5)+2
-- ROUND 15: the two player labels are now built with thickness 0 (see `text_credit_p` above),
-- so their texture pad is (int)(0*1.5)+2 = 2, not 9. Using PAD5 here would shift the ink 7 px
-- right of the cabinet's pen at box.left + border.
local PAD_LABEL = 2
-- The cancel widget is NOT an arcade element (MAPPING.md ROUND 11): the same pill at 0.5,
-- centred under the two rows.
local CANCEL_CX, CANCEL_CY = 960, 838
local ROW_LOOP_F0, ROW_LOOP_LEN = 5, 120
local ROW_LOOP_F0, ROW_LOOP_LEN = 5, 120 -- the row timeline loops `start`..f124
-- NOT arcade: the cabinet lights both rows equally because it has no cursor (each row is
-- armed for that side's own drum). The engine DOES have a cursor, so the row that is not
-- armed is dimmed and the armed one carries a pulse of the arcade white `choose` overlay.
local DIM_UNSELECTED = 0.72
local DECIDE_FLASH_F = 24
local EBG_LEN = math.max(1, EBG:last() - EBG:first() + 1)
local DECIDE_FLASH_F = 24 -- `choose` blink f125..f149
local EBG_LEN = math.max(1, EBG:last() - EBG:first() + 1) -- 360 frames = 6 s
-- `entrybg_instance` is 30 frames and STOPS at f29 (see draw_background); after that the
-- lit-street layer is gone for good. 19 is where its alpha first reaches 0.
local BG_LIT_END = 19
-- {json index, texture frame (0 = shape3, 1 = shape6), track}
local ENTRY_TWINKLES = {
{ 0, 0, "#11@1/#5@0/#3@0" },
{ 1, 0, "#11@1/#9@2/#3@0" },
@@ -134,6 +264,10 @@ function Entry:draw_background()
tex.draw_texture(self.bg_bg)
-- d1 twinkles (arcade draws them under the street; the street is baked into `bg`, so
-- they sit on top here -- only visible where a burst overlaps stall art). A burst is
-- only placed on the frames it is alive, so a track that has no row at `f` returns its
-- clamped end value (alpha 0) and draws nothing.
if self.bg_twinkle then
for i = 1, #ENTRY_TWINKLES do
local tw = ENTRY_TWINKLES[i]
@@ -145,13 +279,31 @@ function Entry:draw_background()
end
end
-- d3-d6 warm glows. The arcade blends these additively; each patch is pre-composited
-- (bg + glow, clamped) so a plain alpha fade reproduces `bg + a*glow` exactly. Two
-- patches only -- the left lamp, and the three right-hand lamps summed into one box --
-- because pre-composited patches must not overlap (a later patch would replace, not
-- add, the glow an earlier one already carries) and every frame of a folder texture
-- is drawn at frame 0's size.
if self.bg_glow then
local ga = EBG:get("#16@3/#14@0", f, "a") or 1.0
for i = 0, 3 do
for i = 0, 1 do
tex.draw_texture(self.bg_glow, { index = i, frame = i, fade = ga })
end
end
-- d7 lit street: a ONE-SHOT "blur in", NOT a loop. `entrybg_instance` (MC 19) is only
-- 30 frames long and carries DoAction index 0 at BOTH f4 and f29. f4 must be Stop() --
-- `entry_main.lua` StartBGIn is what starts the clip (`this.bg_:GotoAndPlay("start")`,
-- label `start` = f5), so the clip is parked at f4 until then -- and f29 carries the
-- SAME action index, therefore f29 is Stop() too. So the cabinet plays f5..f29 once and
-- holds: shape17 (the SHARP, fully-lit street, lapvar 1508) fades 1 -> 0 over f5..f19,
-- leaving shape12 (the DEPTH-OF-FIELD BLURRED street, lapvar 48) as the steady state.
-- That matches every arcade reference capture ("festival street, blurred").
-- `Scripts/anim/entry_bg.lua` was exported with `--range 0,359`, which wrapped this
-- 30-frame clip twelve times and turned the one-shot into a 2 Hz sharp/blurry strobe
-- (the user's 「背景模糊亂跳」). Only THIS channel wraps: the twinkle MCs (5/8/9/10)
-- really are 360 frames and the glow MC (16) really is 120, so `f` stays modulo for them.
if self.bg_street_lit then
local lf = (tex.get_current_ms() - self.bg_t0) * 0.06 -- monotonic, NOT wrapped
if lf < BG_LIT_END then
@@ -161,6 +313,9 @@ function Entry:draw_background()
end
end
-- ROUND 11 -- the arcade credit screen has no panel behind the buttons: the pills sit
-- straight on the festival street. The parent nine-slice box and its
-- japanese caption are therefore not drawn at all.
function Entry:draw_side_select()
end
@@ -183,6 +338,25 @@ function Entry:draw_credit_row(row, f, a)
if msg and ma > 0.002 then
local x = MSG_INK_X - msg.width / 2
local y = cy + TEXT_INK_DY - msg.height / 2
-- ROUND 26 (r26-entrytextfade): `msg`/`msg_hl` are each a whole baked
-- OutlinedText -- white fill AND its outline in ONE texture (OutlinedText
-- bakes them together; text.cpp's draw() only has a single `fade` alpha for
-- the whole quad, there is no separate fill/outline alpha). The old code
-- faded BOTH copies down independently ((1-yellow) and yellow), so around
-- the blink midpoint BOTH copies were simultaneously translucent: the white
-- fill of each stopped being opaque, the pill's yellow bled through it, and
-- the outline ring (itself fading through a yellow close to the pill's own
-- colour) lost all contrast -- exactly the reported "白字跟外框fade時機不同,
-- 出現純白字" flash (captured live at scratchpad/r26etf/before/f20_9530ms.png,
-- ~9.5s into the credit-wait loop, i.e. inside the `choose` blink f64..f95).
-- Fix: draw the black-outline copy ALWAYS fully opaque as the base (fade =
-- a*ma, no `(1-yellow)` term), then cross-dissolve the yellow-outline copy
-- ON TOP of it at fade = a*ma*yellow. Back-to-front opaque compositing means
-- the result is fully opaque at every point of the blend -- the fill stays
-- solid white throughout (yellow copy's white fill lands on the already-
-- opaque white fill beneath, a no-op), and the outline smoothly darkens from
-- black to yellow while never dropping below full alpha, so it is never
-- washed out by the pill background behind it.
msg:draw({ x = x, y = y, fade = a * ma })
if yellow > 0.001 and self.text_credit_msg_hl then
self.text_credit_msg_hl:draw({ x = x, y = y, fade = a * ma * yellow })
@@ -190,28 +364,44 @@ function Entry:draw_credit_row(row, f, a)
end
end
-- The whole credit_instance: both rows + the cancel widget, at `a` (its fade_in/out alpha).
-- `sel` is the highlighted row: 1 / 2 / 0 = the cancel widget / nil = nothing (the decide
-- overlay, where the selection has already been consumed). `flash_row`/`flash_a` drive the
-- `choose` overlay.
function Entry:draw_credit(a, sel, blink_f, flash_row, flash_a, loop_rows)
for row = 1, 2 do
local armed = (row == sel)
local ra = (sel == nil or sel == 0 or armed) and a or (a * DIM_UNSELECTED)
-- Arcade: the cabinet runs the row attract loop (`start`..f124, the yellow-outline
-- blink at f64..f95) on BOTH rows in CreditWait, because neither is "armed". The
-- cursor model only animated the armed row and froze the other at f5.
local rf = (armed or loop_rows) and blink_f or ROW_LOOP_F0
self:draw_credit_row(row, rf, ra)
end
local now = tex.get_current_ms()
if sel and sel > 0 then
-- the selected pill also carries a gentle version of the arcade white `choose`
-- overlay, standing in for a cursor the cabinet does not need (there, a row is
-- picked by hitting that side's own drum).
local pulse = 0.08 + 0.10 * math.sin((now - self.cr_blink_t0) * 0.006)
tex.draw_texture(self.cr_flash, { index = sel - 1, fade = a * pulse })
end
-- The cabinet never shows a 「つかわない」 caption on this screen (the cancel side is
-- just the un-armed state), so nothing is drawn for sel == 0.
if flash_row and flash_a and flash_a > 0.002 then
tex.draw_texture(self.cr_flash, { index = flash_row - 1, fade = a * flash_a })
end
end
-- The engine thinks in SIDES, not in a player COUNT: `side` is 0 = left/1P, 1 = cancel
-- (back to title, or back to mode select when a 2nd player is joining), 2 = right/2P,
-- moved with kat and confirmed with don. The arcade screen has no cursor at all -- each
-- row is armed for that side's own drum -- so side 0/2 map onto the upper/lower row and
-- side 1 onto the cancel widget. See Graphics/entry/MAPPING.md ROUND 11.
function Entry:draw_side_select_buttons(side)
local now = tex.get_current_ms()
__hss_entry_guide = { ms = now, phase = "side" }
if side ~= self.cr_side then
self.cr_side = side
self.cr_blink_t0 = now -- a newly armed row restarts its attract cycle
@@ -219,9 +409,12 @@ function Entry:draw_side_select_buttons(side)
self.cr_seen_frame = self.cr_frame
self.cr_decide_t0 = nil
-- credit_instance fade_in: sprite 11, f5..f15 (10 frames), clamped after
local fi = math.min(15, 5 + (now - self.cr_t0) * 0.06)
local a = CF:get("player1_text_instance", fi, "a") or 1.0
-- Arcade: no cursor at all -- `sel = nil` keeps BOTH rows at full alpha, draws no
-- selection pulse and no 「つかわない」 caption (the cabinet has neither).
local sel = (not self.cr_arcade)
and ((side == 0 and 1) or (side == 2 and 2) or 0)
or nil
@@ -229,11 +422,25 @@ function Entry:draw_side_select_buttons(side)
self:draw_credit(a, sel, blink_f, nil, nil, self.cr_arcade)
-- depth 28 > depth 23: the guide sits OVER the credit rows.
self:draw_control_guide()
-- entry_main.lua only makes `credit_side_[n]` visible in ModeSelectStartWait, i.e. AFTER
-- the credit screen is finished; coin_overlay.lua reads this to keep the 2P invite cloud
-- off the credit screen. EntryScreen::draw runs the screen scripts before the overlay.
YD_ENTRY_CREDIT_MS = now
end
-- The parent's floor band starts at y=862 (575*1.5) and is drawn AFTER the mode-select /
-- costume menu, so it would cover the arcade costume panel bottom (y<=957) and the
-- えらぶ/決定 drum hint (833..921). Squeeze it to start at y=940 (nameplate / フリープレイ
-- still sit on it). Values are screen px (1080p).
-- The arcade entry screen has no floor band: the nameplate and フリープレイ sit
-- straight on the street background. Nothing to draw.
-- ROUND 6 (2P): the engine has no "two players are in" flag Lua can read (entry.cpp resets
-- global_data.player_num to P1 when the 2nd player joins), but it does pass the two join flags
-- here, and EntryScreen::draw calls draw_footer BEFORE coin_overlay.draw() - so publish them.
function Entry:draw_footer(p1_joined, p2_joined)
YD_ENTRY_BOTH_JOINED = (p1_joined == true and p2_joined == true)
-- ROUND 18: once a seat is in, EntryPlayer's own Indicator draws the guide, so the
-- credit screen must stop drawing its own copy (see draw_control_guide).
self.any_joined = (p1_joined == true or p2_joined == true)
end
@@ -242,6 +449,19 @@ function Entry:draw_player_entry()
self:draw_credit_decide()
end
-- ROUND 11 -- the arcade decide beat. `entry_main.lua` runs CreditWait:Decide(1) ->
-- CreditDecideAction (the row plays `choose`, f125..f149: a white overlay blinking on an
-- 8-frame triangle, peak 205/256) -> CreditWait:Finish() -> `fade_out` (f16..f35) -> only
-- THEN ModeSelectStart. Our engine flips EntryState to SELECT_MODE on the same frame the
-- don lands, so the beat has to be played out from here: draw_player_entry() is called in
-- EVERY entry state (EntryScreen::draw), and the mode boxes stay invisible until each
-- player's cloud animation has finished (~1.2 s), which is longer than the 750 ms this
-- takes. The transition is detected by draw_side_select_buttons() no longer being called
-- (Entry:update bumps cr_frame once per frame).
-- ROUND 12 -- EntryScreen::join_player names the row the cabinet's CreditWait:Decide(playerId)
-- would flash (`credit_wait.tlb` plays `choose` on `text_mc_[playerId]`, i.e. the row of the
-- player who hit -- not always row 1). Called on the frame the seat enters, while the engine
-- is still in SELECT_SIDE, so draw_credit_decide() below picks it up next frame.
function Entry:decide_side_select(side)
self.cr_pending_decide = (side == 2) and 2 or 1
end
+8 -3
View File
@@ -67,14 +67,19 @@ function ClipPlayer:draw(now, fade)
local s = shapes[shape]
local id = s and self:id_of(shape)
if id then
local sx, sy = r[4], r[5]
local sx, sy, rot, ox, mirror = r[4], r[5], r[6], s[3], nil
-- a mirrored leaf (det < 0) is exported as a negative sx with the angle of the
-- mirrored axis; drawn as a horizontal flip of the |sx| copy, the pivot moves to
-- w - ox inside the flipped image and the angle comes back by 180 degrees
if sx < 0 then sx, rot, ox, mirror = -sx, rot - 180, s[1] - s[3], "horizontal" end
tex.draw_texture(id, {
x = self.x + r[2],
y = self.y + r[3],
x2 = s[1] * (sx - 1),
y2 = s[2] * (sy - 1),
origin = { s[3] * sx, s[4] * sy },
rotation = r[6],
origin = { ox * sx, s[4] * sy },
rotation = rot,
mirror = mirror,
fade = r[7] * fade,
blend = (r[8] == 8) and "additive" or nil, -- cabinet add-blended leaf
})
+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((tex.skin_text("song_num_max"):gsub("{0}", tostring(limit))),
self._song_max = text.create_raw_text(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
+2 -11
View File
@@ -40,7 +40,6 @@ function Indicator:update(current_ms) end
-- song-select keys are set to y=1 (see skin_config), entry/others to y=0.
local SPLIT, DECIDE_END = math.floor(420 / FRAME_STEP), math.floor(650 / FRAME_STEP) -- cells 210..324 = both sticks + 決定
__hss_costume_panel = __hss_costume_panel or {}
__hss_entry_guide = __hss_entry_guide or nil -- { ms, phase = "side" | "mode", count } from the entry scripts
-- ROUND 11 drew the 段位道場 shutter doors from HERE, purely for layering: the
-- indicator was the last skin-owned draw on SONG_SELECT and therefore the only slot
@@ -66,19 +65,11 @@ function Indicator:draw(x, y, fade)
for _, t in pairs(__hss_costume_panel) do
if now - t < 100 then return end
end
-- entry phases (published by entry.lua / entry/box.lua while they draw): TaikoSelect hides
-- the guide, ModeSelect with several boards shows the `all` loop instead of `decide`
local g = __hss_entry_guide
local show_all = false
if g and now - g.ms < 100 then
if g.phase == "side" then return end
show_all = (g.phase == "mode") and (g.count or 1) > 1
end
local elapsed = tex.get_current_ms() - self.t0
local frame
if (y or 0) >= 1 or show_all then
if (y or 0) >= 1 then
frame = math.floor(elapsed / 1000 * ARCADE_FPS / FRAME_STEP) % SPLIT
if (y or 0) >= 1 then y = y - 1 end
y = y - 1
else
local n = DECIDE_END - SPLIT
frame = SPLIT + math.floor(elapsed / 1000 * ARCADE_FPS / FRAME_STEP) % n
+2 -23
View File
@@ -153,16 +153,6 @@ 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
@@ -180,7 +170,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(tex.skin_text("skip_badge_text"), SKIP_TEXT_SIZE,
self.skip_text = text.create_raw_text("演奏スキップON", SKIP_TEXT_SIZE,
{ 0, 0, 0, 255 }, { 0, 0, 0, 0 }, false, 0, 2)
end
return self
@@ -286,18 +276,7 @@ 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`.
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
put(self.hint, REST_HINT, 0, 568, 1920, 512, info)
end
-- 7. 演奏スキップON badge, top-left, on the same fade as the band. The arcade
+11 -15
View File
@@ -10,8 +10,7 @@ ResultPlayer = {}
ResultPlayer.__index = ResultPlayer
local GAUGE_SCALE = 0.7 -- arcade places tamashii_gage at 0.7 on the result board
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 GAUGE_UNIT = 12 * GAUGE_SCALE -- 12 px per YataiDON gauge unit in the plate groove (87 units)
local OPTION_PITCH = 43
-- ROUND 15 — the whole result sequence is now the cabinet's own state machine, read out of
@@ -38,7 +37,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 -- 58.33 ms per cell (kGageSpeed_ = 7 f per arcade unit)
local GAUGE_MS_UNIT = 7 * F * (50 / 87) -- 33.53 ms per YataiDON unit == 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
@@ -170,9 +169,6 @@ 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
@@ -218,7 +214,7 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
self.gauge_started = false
self.gauge_start_ms = -1
self.gauge_shown = 0 -- cells currently filled (animates up to gauge_cells)
self.gauge_shown = 0 -- units currently filled (animates up to gauge_length)
self.gauge_done = false
self.score_delay = nil
self.fade_in_started = false -- read by C++: starts the score roll
@@ -353,7 +349,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 = self.gauge_cells
self.gauge_shown = math.floor(self.gauge_length)
-- 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
@@ -376,7 +372,7 @@ function ResultPlayer:update(current_ms, fade_in_finished,
if self.gauge_started then
self.tamashii_fire:update(current_ms)
local target = self.gauge_cells
local target = math.floor(self.gauge_length)
-- 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
@@ -392,7 +388,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 >= GAUGE_CELLS then
if not self.achieve_played and self.gauge_state == 2 and self.gauge_shown >= 87 then
self.achieve_played = true
audio.play_sound("achieve_tamashii" .. self.seat_lr, "sound")
end
@@ -414,7 +410,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 >= GAUGE_CELLS then
if self.gauge_state == 2 and self.gauge_shown >= 87 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
@@ -622,7 +618,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 = { 30, 35, 40 } -- easy / normal+hard / oni norma cells
local clear_pts = { 52, 60, 69 }
local clear_pt = clear_pts[self.art_index + 1]
local u = GAUGE_UNIT
@@ -630,7 +626,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 >= GAUGE_CELLS then
if self.gauge_state == 2 and gl_int >= 87 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.
@@ -696,7 +692,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 >= GAUGE_CELLS and self.t_gauge_tamashii_fire then
if self.gauge_state == 2 and gl_int >= 87 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
@@ -712,7 +708,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 >= GAUGE_CELLS and self.t_gauge_tamashii_overlay then
if self.gauge_state == 2 and gl_int >= 87 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,
+44 -155
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((tex.skin_text("song_num_max"):gsub("{0}", tostring(limit))),
self._song_max = text.create_raw_text(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
@@ -361,7 +361,6 @@ function SongSelect:draw_top(dan_progress)
end
function SongSelect:draw_overlays(state)
self:draw_decided_bridge() -- decided genre board while its songs load (defined below)
if _dd_ok then dan_doors.arm(state == STATE_DAN_SELECTED) end
-- number_of_songs plate: arcade song_of_number_instance at stage (1754,270), 280x72 centred.
-- The engine draws its own song_num_bg (made transparent in this skin) and the "N曲目" text
@@ -679,7 +678,6 @@ end
-- Scrolling genre backdrop (enso_normal/song_select/song_select_bg, 2880x1080 per genre)
--------------------------------------------------------------------------------
function SongSelect:draw_background(nav)
self._frame_no = (self._frame_no or 0) + 1
self:refresh_folder_context(nav)
local info = tex.get_texture_info("box", "background")
local width = info.width
@@ -712,17 +710,13 @@ end
-- width -- exactly how the existing `y2 = h - 352` calls already drive the height. So a
-- horizontal scale is `x2 = BOARD_W * (sx - 1)` plus a re-centred x, with no engine
-- change. sx = nil/1 leaves every pre-r61 call byte-identical.
-- A furled board (sx < 1) is not a squashed copy of the 960-wide texture: squashing
-- averages the outline away and makes the GPU pick a small mip level (the LOD follows
-- the larger axis), which blurs every row. The cabinet scales a vector shape, so its
-- edges stay sharp at any width. Five columns get the same result from the bitmap:
-- the two rounded corners at 1:1 (40 px), and the coloured band / inner panel / band
-- between them scaled in proportion, each drawn from an 8 px flat slice of itself and
-- MAGNIFIED to its width. Column layout of every bar_genre frame and the back board:
-- corner 0..40 | band 40..83 | panel 83..877 | band 877..920 | corner 920..960.
local function draw_board(bar_id, frame, bx, by, cen_h, fade, crop_center, sx)
local w = BOARD_W
if sx and sx ~= 1.0 then w = math.max(2, math.floor(BOARD_W * sx + 0.5)) end
local x2 = nil
if sx and sx ~= 1.0 then
w = BOARD_W * sx
x2 = w - BOARD_W
end
local x = math.floor(bx - w / 2 + 0.5)
local top = math.floor(by - cen_h / 2 + 0.5) - CAP_H
local src_y, src_h = CAP_H, CEN_SRC_H
@@ -731,53 +725,12 @@ local function draw_board(bar_id, frame, bx, by, cen_h, fade, crop_center, sx)
src_y = CAP_H + math.floor((CEN_SRC_H - src_h) / 2)
cen_h = src_h
end
-- the three horizontal bands (top cap / centre / bottom cap) as {y, src_y, src_h, dst_h}
local bands = { { top, 0, CAP_H, CAP_H }, { top + CAP_H, src_y, src_h, cen_h },
{ top + CAP_H + cen_h, CAP_H + CEN_SRC_H, CAP_H, CAP_H } }
if w == BOARD_W then
for _, b in ipairs(bands) do
tex.draw_texture(bar_id, { frame = frame, x = x, y = b[1], y2 = b[4] - 352, fade = fade,
src = { x = 0, y = b[2], width = BOARD_W, height = b[3] } })
end
return
end
local CORNER, BAND, PANEL = 40, 43, 794 -- source widths; 40+43+794+43+40 = 960
local cols
if w >= 2 * CORNER then
local corner = CORNER
local inner = w - 2 * corner
local band = math.floor(inner * BAND / (2 * BAND + PANEL) + 0.5)
local panel = inner - 2 * band
-- columns: {src_x, src_w, dst_x, dst_w}
cols = {
{ 0, CORNER, x, corner },
{ CORNER + BAND / 2 - 4, 8, x + corner, band },
{ BOARD_W / 2 - 4, 8, x + corner + band, panel },
{ BOARD_W - CORNER - BAND / 2 - 4, 8, x + corner + band + panel, band },
{ BOARD_W - CORNER, CORNER, x + w - corner, corner },
}
else
-- narrower than both corners (the last frames of a furl / first of an unfurl): the
-- cabinet's vector shape is a solid bar with its outline all round, so draw the
-- two outline columns (texture x 13..20 / 940..947) at a few px each and fill the
-- rest from the flat centre slice -- squeezing the rounded corners instead left
-- two hollow strokes with nothing between them.
local ol = math.max(2, math.min(8, math.floor(w / 6)))
local mid = math.max(0, w - 2 * ol)
cols = {
{ 13, 8, x, ol },
{ BOARD_W / 2 - 4, 8, x + ol, mid },
{ 940, 8, x + ol + mid, ol },
}
end
for _, b in ipairs(bands) do
for _, c in ipairs(cols) do
if c[4] > 0 then
tex.draw_texture(bar_id, { frame = frame, x = c[3], y = b[1], x2 = c[4] - BOARD_W, y2 = b[4] - 352,
fade = fade, src = { x = c[1], y = b[2], width = c[2], height = b[3] } })
end
end
end
tex.draw_texture(bar_id, { frame = frame, x = x, y = top, y2 = CAP_H - 352, x2 = x2, fade = fade,
src = { x = 0, y = 0, width = BOARD_W, height = CAP_H } })
tex.draw_texture(bar_id, { frame = frame, x = x, y = top + CAP_H, y2 = cen_h - 352, x2 = x2, fade = fade,
src = { x = 0, y = src_y, width = BOARD_W, height = src_h } })
tex.draw_texture(bar_id, { frame = frame, x = x, y = top + CAP_H + cen_h, y2 = CAP_H - 352, x2 = x2, fade = fade,
src = { x = 0, y = CAP_H + CEN_SRC_H, width = BOARD_W, height = CAP_H } })
end
-- Yellow cursor glow around the board (arcade cursor_top/center/bottom): the glow's caps
@@ -1026,7 +979,6 @@ local R61 = {
-- clip landmarks (overwritten from the generated table)
open_f0 = 45, open_swap = 114, open_end = 145,
close_f0 = 150, close_swap = 219, close_end = 250,
open_furl = 100, -- first frame the decided genre board furls shut (genre_board_sx < 1)
-- event bookkeeping
seq = nil, -- last Navigator.wheel_event_seq we saw
have_events = false, -- engine actually exposes the r85 channel
@@ -1041,12 +993,6 @@ do
"close_f0", "close_swap", "close_end" }) do
if t[k] then R61[k] = t[k] end
end
local rows = t.tracks and t.tracks.genre_board_sx
if rows then
for i = 1, #rows do
if rows[i][2] < 0.999 then R61.open_furl = rows[i][1]; break end
end
end
end
end
-- Linear sampler over the generated {frame, v1, v2...} rows. Fail-soft: an absent table
@@ -1098,14 +1044,8 @@ local function leg_f()
if R61.disabled or not R61.clip or not R61.t0 then return nil end
local f = R61.f0 + (tex.get_current_ms() - R61.t0) * 0.06
if not R61.swapped then
-- hold until the engine says the swap happened. The close leg holds one frame
-- short of its swap; the OPEN leg holds one frame short of the FURL (f99): the
-- cabinet loads a folder inside its 69-frame decide, this engine can take a
-- couple of seconds, and parking on f113 (board already furled away, boxes
-- flown out) left the screen empty for all of that time. Parked on f99 the
-- decided board stays up as the loading state and the furl -> unfurl pair
-- plays back to back once the swap lands (see leg_event).
local hold = (R61.kind == "close") and (R61.close_swap - 1) or (R61.open_furl - 1)
-- hold one frame short of the swap until the engine says it happened
local hold = (R61.kind == "close") and (R61.close_swap - 1) or (R61.open_swap - 1)
if f > hold then f = hold end
return f
end
@@ -1114,15 +1054,6 @@ local function leg_f()
return f
end
-- The engine's own box fade, except during the pre-swap half of the CLOSE leg: there the
-- engine fades the song boxes and the back board out in 166 ms, while the cabinet keeps
-- them solid until they fly out (f184..204) / the board furls (f205..217) -- those come
-- from the clip channels, so the engine fade is ignored until the swap.
local function box_fade(box)
if R61.kind == "close" and R61.t0 and not R61.swapped then return 1.0 end
return box.fade.attribute
end
-- Start / advance the leg. `begin` puts the clock on the label frame; `swap` re-anchors
-- it on the cabinet's content-swap frame so the post-swap half is phase-exact no matter
-- how long the loader took.
@@ -1130,16 +1061,8 @@ local function leg_event(id)
local now = tex.get_current_ms()
if id == 3 then -- OPEN_BEGIN
R61.t0, R61.f0, R61.kind, R61.swapped = now, R61.open_f0, "open", false
R61.dec_id, R61.dec_box = R61.last_open_id, R61.last_open_box -- the decided board
elseif id == 4 then -- OPEN_SWAP
-- the clock was parked on open_furl - 1 while the loader ran: resume from the
-- furl so furl (f100..113) and unfurl (f114..) play back to back. A swap that
-- lands before the park point (tiny folder) just lets the clock keep running.
local f = (R61.kind == "open" and R61.t0) and (R61.f0 + (now - R61.t0) * 0.06) or nil
if not f or f >= R61.open_furl - 1 then
R61.t0, R61.f0 = now, R61.open_furl
end
R61.kind, R61.swapped = "open", true
R61.t0, R61.f0, R61.kind, R61.swapped = now, R61.open_swap, "open", true
R61.snap_once, R61.snap_ms = true, now -- OpenFolderState ends GotoAndPlay("on")
elseif id == 5 then -- CLOSE_BEGIN
R61.t0, R61.f0, R61.kind, R61.swapped = now, R61.close_f0, "close", false
@@ -1168,17 +1091,8 @@ local _fly_dbg = read_env("YATAIDON_R56_DEBUG") ~= nil -- log sampled offsets (
local function fly_offset(box)
if R56_DISABLED then return 0.0, 0.0 end
local f = leg_f()
if not f then return 0.0, 0.0 end
if R61.kind == "open" then
if f >= FLY_END_F then return 0.0, 0.0 end
elseif R61.kind == "close" then
-- the CLOSE leg is the clip's `return` half: the song boxes fly out f184..204,
-- wait off screen across the swap (f219) and the genre boxes fly back in
-- f234..254; f255 (`genre_deceide2`) re-bases the clip, so stop just before it
if f >= (WHEEL_FLY:label("genre_deceide2") or 255) - 1 then return 0.0, 0.0 end
else
return 0.0, 0.0
end
if not f or not R61.swapped or R61.kind ~= "open" then return 0.0, 0.0 end
if f >= FLY_END_F then return 0.0, 0.0 end
local st = box:draw_state()
if st == "open" or st == "diff_select" then return 0.0, 0.0 end -- the decided board stays
local d = box:box_y() - FLY_CENTER_Y
@@ -1195,25 +1109,6 @@ local function fly_offset(box)
return x - rx, y - ry
end
-- The decided genre board while the loader owns it (see draw_folder_open): drawn here,
-- after the wheel, whenever draw_box did not get to it this frame, until it has furled.
function SongSelect:draw_decided_bridge()
if not R61.dec_box then return end
local lf = (R61.kind == "open") and leg_f() or nil
if lf and lf < R61.open_swap - 1 then
if self._dec_frame ~= self._frame_no then
pcall(self.draw_folder_open, self, R61.dec_box)
end
else
R61.dec_id, R61.dec_box = nil, nil
end
end
-- true while `box` is the genre board the player just decided on (open leg running)
local function is_decided(box)
return R61.kind == "open" and R61.t0 ~= nil and R61.dec_id ~= nil and box_id(box) == R61.dec_id
end
-- ROUND 51: how a FRESH open animation starts is not one-size-fits-all. From the real
-- the reference build song_select_song_main.lua state machine (scratchpad dec/, verified this round):
-- * cursor move Scroll -> Main -> ProcKanbanWaitCountToSelectOn (61-tick hold,
@@ -1453,7 +1348,7 @@ end
function SongSelect:draw_song_board(box, pb, ia, ic)
local bx, by = box:box_x(), box:box_y()
local fdx, fdy = fly_offset(box); bx, by = bx + fdx, by + fdy -- r56 wheel fly-in
local fade = math.min(1.0, box_fade(box))
local fade = math.min(1.0, box.fade.attribute)
-- ROUND 61: the folder-ENTER unfurl. On the frames the cabinet is playing
-- genre_deceide f113..f145 the incoming song board is NOT growing vertically from
@@ -1480,9 +1375,6 @@ function SongSelect:draw_song_board(box, pb, ia, ic)
tostring(box:draw_state()), tostring(box.text_name)))
end)
end
if gof and R61.kind == "open" and gof < R61.open_swap - 1 and box:draw_state() == "open" then
return -- the new content's board does not exist until the swap frame
end
if gof and box:draw_state() == "open" then
local sx, ba = go_get("song_board_sx", gof, 1), go_get("song_board_sx", gof, 2)
if sx and ba then
@@ -2185,7 +2077,7 @@ function SongSelect:draw_folder_board(box, pb, ch_alpha, ch_off, ta, glow_a)
local bx, by = box:box_x(), box:box_y()
local fdx, fdy = fly_offset(box); bx, by = bx + fdx, by + fdy -- r56 wheel fly-in
local cen_h = CEN_CLOSED_H + (CEN_OPEN_H - CEN_CLOSED_H) * pb
local fade = box_fade(box)
local fade = box.fade.attribute
local frame, has_pattern = folder_frame(box)
-- ROUND 85 ---------------------------------------------------------------
@@ -2202,7 +2094,7 @@ function SongSelect:draw_folder_board(box, pb, ch_alpha, ch_off, ta, glow_a)
-- pointing at.
local ltitle = 1.0
if not R61.disabled then
local lf = (box:draw_state() == "open" or is_decided(box)) and leg_f() or nil
local lf = (box:draw_state() == "open") and leg_f() or nil
if lf then
local sx, ba = go_get("genre_board_sx", lf, 1), go_get("genre_board_sx", lf, 2)
if sx and ba then
@@ -2331,15 +2223,6 @@ function SongSelect:draw_folder_closed(box)
end
function SongSelect:draw_folder_open(box)
-- remember the decided board: the engine removes the folder box from the wheel
-- while its songs load, and draw_overlays keeps drawing it from here until it
-- has furled away (the C++ box outlives the wheel slot, so the handle stays valid)
R61.last_open_id, R61.last_open_box = box_id(box), box
if is_decided(box) then
self._dec_frame = self._frame_no
self:draw_folder_board(box, 1.0, 1.0, 440.0, 1.0, 1.0)
return
end
local key = "f\0" .. box_id(box) -- r63: per-BOX, not per-title (see box_id)
local t = wheel_open_t(key)
if t == nil then -- cabinet 508 ms pre-growth hold: fully closed
@@ -2390,7 +2273,7 @@ function SongSelect:draw_back_board(box, p)
local bx, by = box:box_x(), box:box_y()
local fdx, fdy = fly_offset(box); bx, by = bx + fdx, by + fdy -- r56 wheel fly-in
local cen_h = CEN_CLOSED_H + (CEN_OPEN_H - CEN_CLOSED_H) * p
local fade = math.min(1.0, box_fade(box))
local fade = math.min(1.0, box.fade.attribute)
-- ROUND 85: on the cabinet the panel that unfurls across the swap is the
-- newly-focused one (`GetSongKanbanMc(kSelectNumber + openOffset)`). In THIS
@@ -2403,7 +2286,6 @@ function SongSelect:draw_back_board(box, p)
local lta = 1.0
if not R61.disabled then
local lf = (box:draw_state() == "open") and leg_f() or nil
if lf and R61.kind == "open" and lf < R61.open_swap - 1 then return end -- not yet
if lf then
local sx, ba = go_get("song_board_sx", lf, 1), go_get("song_board_sx", lf, 2)
if sx and ba then
@@ -2498,23 +2380,30 @@ 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.
-- 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 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 = "首" },
}
local function sw_words(group)
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)
local lang = (tex.language and tex.language()) or "en"
local t = SW_WORDS[group]
return t[lang] or t.en
end
-- OutlinedText cache: key = group .. "\0" .. index (the strings never change at runtime).
@@ -2755,7 +2644,7 @@ function SongSelect:draw_box(box)
elseif state == "open" then self:draw_song_open(box)
else self:draw_song_closed(box) end
elseif kind == "folder" then
if state == "open" or is_decided(box) then self:draw_folder_open(box)
if state == "open" then self:draw_folder_open(box)
else self:draw_folder_closed(box) end
elseif kind == "back" then
if state == "open" then self:draw_back_open(box)
@@ -2776,7 +2665,7 @@ function SongSelect:draw_box(box)
p = e and (folder_close_timeline(e)) or 0.0
end
local cen_h = CEN_CLOSED_H + (CEN_OPEN_H - CEN_CLOSED_H) * p
local fade = math.min(1.0, box_fade(box))
local fade = math.min(1.0, box.fade.attribute)
if p > 0 then self:draw_glow(bx, by, cen_h, fade * p * glow_pulse()) end
draw_board((p > 0) and self._id_bar_folder or self._id_bar_genre, 10, bx, by, cen_h, fade, p <= 0 or p >= 1) -- ROUND 5: closed = full pale panel
if p > 0 and p < 1 then draw_board(self._id_bar_genre, 10, bx, by, cen_h, fade * (1.0 - p)) end
+27 -27
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
8fe3634c792f5e22d301d5775e61243ba4ae1d2e131a77efa6b5cb8d2257e3c0 *./Graphics/background/bga_bg_fever_2/texture.json
42692e59d714c56aae4d46fa24dafbb583e3f52c9347b6812761d0c4a999aed7 *./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
7bf2b9ac45c4ee6ce5073d910e291eb922174cba954be90696c74fda27317835 *./Graphics/entry/background/texture.json
671f31ba324eae4b68797ead149b72e691e158492c9541327b07c5fc2727acea *./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
89c4d6afafb8010ec4da0b9f72f10f677ec8c87677fcb3d2c9edde923a5b5f9f *./Graphics/result/score/texture.json
b309049109d5ba5a35a29b06373f13dc93ec8591e93a3267616b7b33e0b8d18e *./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
66797bb60d6bacde5d882136a7dee9cda2f3e02282dd2ebaf2ee37346db14f70 *./Scripts/anim/wheel_decide.lua
a340f195886e0e372da2cca847c1a243ed56d76239597c01f6ccdf11d1779759 *./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
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
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
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
8c15718242606d122b6f9766c105500b3187f6ab0f4fd01bb84946f77ac39a34 *./Scripts/background/bg_objects/bg_anim.lua
735bec40edcaa27036a8b0d83ba34ac57a40ae985bf538edf98d06e95b54c3d7 *./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
149dc01c612347cdfcdf439315ef68dfc29faab2ea82453f5590b5583551c0da *./Scripts/background/bg_objects/ending.lua
5f95cbe4c2e75e0c526b4bcd17125f02decd21d4c1db8c2c1b15a3762e590c0b *./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
@@ -3246,24 +3246,24 @@ f204455c907d3e0e7affb89db3649a709a27d528d84cc58d51a7d8cfaa1d3deb *./Scripts/back
6e2b3c136611c688cdfbe44b0bbcb9d25c2ab647903246b07f2aef9baa6bab0b *./Scripts/background/dancer_data/dancer_4.lua
3a4f1f45bb54c4303dc97afcc6f67df831d6d6011a2264baed3d933c789d210e *./Scripts/background/dancer_data/dancer_5.lua
510225154288da646a6becc9df9f3e2beb00af165815c0e9545a0363dddba589 *./Scripts/dan_select/dan_select.lua
616d72c0192f4a5a0309d4d16d481688a4c6a284faa4866b6e32ffa1608bddad *./Scripts/entry/box.lua
06a8e89044f740783e9ea293c1ebfe5aefe7b68fc34c38d72327e03aea75466f *./Scripts/entry/box.lua
744599a57762ca2a669240caee885431c4216945c05bcd4e85c45850f2e931a3 *./Scripts/entry/costume_menu.lua
64771d8bda30f17fa8e01862e722b7398c65d314fa2482adec348074859acf7f *./Scripts/entry/entry.lua
8b28e4ba452844cb59b0db4a6b90043a589d944ad458c4788b6cc6d31b9bd4b1 *./Scripts/entry/entry.lua
3d8c25402fff69adbfa1f6796807665f6cd61528583d0b7c625e09af752d5160 *./Scripts/entry/player.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
056197114b3591a56de9bdd370dbfb3581cc4f9d78ded8a5c690ecd157122759 *./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
60aad9f07edee45abb2dca0803acc0f46a519d6a0f6e0c7625b8dd127447cfb9 *./Scripts/game/ending_anim/rank_hud_in.lua
f4de8faceba1417bbdf0c408275f36b91022c93539178bd2d59018623bac759b *./Scripts/game/ending_anim/rank_hud.lua
9a55c93ebd302db1bb4c7f55093582a1743ae29b8a34b7544e2e5117f78b865d *./Scripts/game/ending_anim/rank_hud.lua
826c02cac90412a42722d0fa23e5f08878b00f32ca564607d08bd36fdecc0de4 *./Scripts/game/ending_anim/rank_hud_out.lua
6c885099876d77ea14d4588edb3de5105ecef7f8b83e2787a3bdb25918e5b920 *./Scripts/game/ending_anim/rank_result_kiwami.lua
02a2a424b68fce7b172807ce60df5c2b41121cb65f2edcc5a1e43b77702a4008 *./Scripts/game/ending_anim/rank_result.lua
5010747253fcca353b124290d1874b8a1ef5f733b77bd1ada14fd07e0f55e57e *./Scripts/game/ending_anim/rank_result_kiwami.lua
ff7652f6d7bab5ebf7391d360a625a7cfe9a694bc40951aa7a1fa096fa4fe9a6 *./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
bc6dea42b2183980e1112d815106b357876c18590daa831a48a113eea99c6815 *./Scripts/global/indicator.lua
4d4d092391f7c74f270066927d84203a7b5b0011bab3db90276b2b80cba35601 *./Scripts/global/indicator.lua
ab7af813fb8d836fb6490554e11b374beb0ea1af0346ecadc3431498e91e5e02 *./Scripts/global/nameplate.lua
6b3cbc22709edf45514674830e1eb5831a6e45bb7f3b6b030a1ecda94beb5185 *./Scripts/global/result_transition.lua
cce16946fc792a3412c248d059ce4387f482f969e72e787e4770c20116c0e682 *./Scripts/global/timer.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
141a453f447f176f13f6c35f20335261d98f78ec97671e9bdb54cd5deece802c *./Scripts/result/result_crown.lua
8cc680fde26a887539c6335bbf29a4ea2f96ef95164ec84580ed821f6b2355e5 *./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
093ad96b9aebc98e6b1fb2ce8f8f3f9e9ab4e0f24f7328e6eb831d8c5e90c87e *./Scripts/song_select/song_select.lua
d3af610009783a9cf6e044dfcee396ea7f0ee90cd059fc44feb98e6935a5afff *./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
@@ -1,83 +0,0 @@
# `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
@@ -1,157 +0,0 @@
"""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
@@ -1,155 +0,0 @@
"""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()