Compare commits

..
Author SHA1 Message Date
TaikoZhongandClaude Opus 5.5 dc4fc59741 entry: pin the entry animations the Don placement is built on
Nijiiro had no Graphics/entry/animation.json of its own and took the
parent's. The parent changed its entry animations (drum bounce -245 ->
-340, cloud puff 0.75-1.0 -> 0.1-1.2, cloud loop, timings, mode-select
fade 160 -> 443 ms), but player.lua lands Don on DON_CY with a fixed
BOUNCE_END = -367.5 (the old -245 x 1.5), so after picking a side Don
now stopped ~140 px too high and the cloud was sized differently.

This adds the parent's previous entry animation.json unchanged, so
Nijiiro's entry looks as it did before the parent's change. Nijiiro's
own entry scripts and the engine's entry ids (9-11) are all in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:48:00 +08:00
TaikoZhongandClaude Opus 5.5 d87aef0177 Update checksums.sha256
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:34:18 +08:00
GanYiZhong 605a651a25 Merge pull request 'dan select: add the dan transition animations (19-23)' (#45) from GanYiZhong/YataiDONNijiiro:dan-transition-anims into main 2026-10-12 04:31:32 +02:00
TaikoZhongandClaude Opus 5.5 b9fee6e113 dan select: add the dan transition animations (19-23)
The engine's dan transition (shown when leaving dan select) uses dan_select
animations 19-23. This skin has its own dan_select/animation.json without
them, so leaving dan select crashed: the transition textures come from
PyTaikoGreen, but the animations did not. Added from PyTaikoGreen with the
distances scaled x1.5 for 1920x1080 (22 is degrees and stays as is).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:25:39 +08:00
Yonokid 88df3fde7f state is now controlled by lua 2026-10-11 13:24:55 -04:00
Yonokid 9cfd0d9a76 minor optimization 2026-10-10 14:52:41 -04:00
Yonokid 6976222461 add timing offset option 2026-10-10 10:43:50 -04:00
Yonokid f604f65435 minor fixes 2026-10-09 16:14:31 -04:00
Yonokid 200cdc58ed add missing animation 2026-10-09 15:53:51 -04:00
Yonokid c531160071 fix result rank issue 2026-10-08 11:46:56 -04:00
Yonokid 2eccdaecd6 add gogo time lane effect 2026-10-07 16:29:57 -04:00
Yonokid e35c5ab854 minor fixes 2026-10-07 14:06:42 -04:00
Yonokid 30590cd9b4 add donbg tinting 2026-10-07 13:46:09 -04:00
Yonokid dcdff8924c Merge pull request 'dan select: extend the parent's DanSelect instead of replacing it' (#42) from GanYiZhong/YataiDONNijiiro:fix/dan-select-parent into main
Reviewed-on: Yonokid/YataiDONNijiiro#42
2026-10-05 13:48:43 +02:00
Yonokid 267d0c2f3e Merge branch 'main' into fix/dan-select-parent 2026-10-05 13:46:15 +02:00
Yonokid fafd8d2778 Merge pull request 'Add yellow_box_bottom to skin_config (dan select box bottom edge)' (#41) from GanYiZhong/YataiDONNijiiro:fix/yellow-box-bottom into main
Reviewed-on: Yonokid/YataiDONNijiiro#41
2026-10-05 13:46:08 +02:00
Yonokid b87ef7a172 Merge branch 'main' into fix/yellow-box-bottom 2026-10-05 13:45:56 +02:00
Yonokid 941afd274c Update checksums.sha256 2026-10-04 16:03:25 -04:00
TaikoZhongandClaude Opus 5.5 3be0239090 skin_config: add yellow_box_bottom for the dan select box
The engine now reads the yellow box's bottom edge from skin_config
yellow_box_bottom instead of the yellow_box_bottom_right texture's y.
Without the key the bottom row was placed by the fallback and the box
looked cut off under the song scrolls. 974 is the value the texture
entry used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:36:27 +08:00
GanYiZhongandClaude Opus 5.5 525a9f36ff dan select: extend the parent's DanSelect instead of replacing it
The dan boxes moved to PyTaikoGreen's Scripts/dan_select/dan_select.lua, which
this skin's dan_select.lua shadowed, so no dan box was drawn. Load the parent's
script first and add only the clip-sampled cursor/arrows on top of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:35:05 +08:00
Yonokid 2f41fe2bbd track ttf with LFS 2026-10-04 01:14:24 -04:00
Yonokid 8a75d48555 fix background animations 2026-10-01 16:48:11 -04:00
Yonokid cc3e9308f5 Update checksums.sha256 2026-10-01 14:51:49 -04:00
Yonokid 5b0412389e remove some extra loading 2026-10-01 14:47:49 -04:00
Yonokid 506c761c3a Update checksums.sha256 2026-10-01 14:15:56 -04:00
Yonokid f37375e18a remove blank graphics 2026-10-01 14:07:37 -04:00
Yonokid fb7c1ba611 removed debug code 2026-10-01 11:28:23 -04:00
Yonokid fd383c7b8b separate the song select lua 2026-10-01 01:08:03 -04:00
Yonokid 8f4b6835c4 Update checksums.sha256 2026-09-30 15:55:46 -04:00
Yonokid ec3fa91b0d Merge pull request 'Background: the collab backgrounds PyTaikoGreen added after background.lua was forked' (#40) from GanYiZhong/YataiDONNijiiro:fix/collab-backgrounds into main
Reviewed-on: Yonokid/YataiDONNijiiro#40
2026-09-30 21:54:58 +02:00
Yonokid 814f21047b Merge branch 'main' into fix/collab-backgrounds 2026-09-30 21:54:46 +02:00
Yonokid 89618b4c02 Merge pull request 'Animations: follow the engine's merged move animations' (#39) from GanYiZhong/YataiDONNijiiro:fix/merged-move-animations into main
Reviewed-on: Yonokid/YataiDONNijiiro#39
2026-09-30 21:54:18 +02:00
Yonokid 3ea0910bd5 Merge branch 'main' into fix/collab-backgrounds 2026-09-30 21:53:56 +02:00
Yonokid c3d9c9f1b4 Merge branch 'main' into fix/merged-move-animations 2026-09-30 21:53:40 +02:00
Yonokid a340d03a93 change scos 2026-09-30 15:50:57 -04:00
Yonokid 5b46d889d8 major cleanup 2026-09-30 15:29:22 -04:00
Yonokid 366ece5ebe move the attract sequence back to pytaikogreen 2026-09-30 14:35:21 -04:00
Yonokid 06eb0778e4 Update checksums.sha256 2026-09-30 14:25:21 -04:00
Yonokid f45fe56a67 update title video 2026-09-30 11:50:02 -04:00
Yonokid 401d7cf12f Update checksums.sha256 2026-09-29 15:33:43 -04:00
TaikoZhongandClaude Opus 5.5 9999570d4c Background: the 27 collab backgrounds PyTaikoGreen added after this file was forked
background.lua was forked from PyTaikoGreen on 2026-09-02; PyTaikoGreen then
added DQ10 (09-07) and 26 more (09-09, "Add all missing collab
backgrounds"), so a chart with SCENEPRESET:MIKU / TOHO / YOKAI / ... fell
back to the standard background here. They now use PyTaikoGreen's modules.

bg_collabs/gumi.lua: PyTaikoGreen's, with GumiDonBG filling in the fields
PyTaikoGreen's DonBG6 sets (this skin has its own donbg/6), which its
draw_textures uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:30:39 +08:00
TaikoZhongandClaude Opus 5.5 a887052197 Animations: follow the engine's merged move animations (cf785cf)
The engine now reads one move per effect where it used to add several:
kusudama 11 (= old 11 + 12) and 13 (= old 13 + 18), diff sort bounce 22
(= old 22-25), score counter 38 (y before the fan-out, from the old start
y and 39) and 39 (fan-out = old 40); the single-point "waypoint" key is
now a "waypoints" list. Same motion as before, at this skin's values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:41:16 +08:00
Yonokid d121d5ed34 Merge pull request 'Ending: place the 2P banner on the 2P lane (game_p2_y)' (#38) from GanYiZhong/YataiDONNijiiro:fix/2p-ending-lane-y into main
Reviewed-on: Yonokid/YataiDONNijiiro#38
2026-09-29 16:49:50 +02:00
TaikoZhongandClaude Opus 5.5 4c50c7f2cb Ending: place the 2P banner on the 2P lane (game_p2_y)
game_2p_offset is the distance between the two background bands (536), not between the lanes,
so in two-player games the 2P clear / full combo / fail banner was drawn below the screen.
The 2P lane is at game_p2_y, the 1P lane at 184 (both x screen scale).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 05:39:26 +08:00
Yonokid 35b6152aae Update checksums.sha256 2026-09-24 18:31:24 -04:00
Yonokid 3af3f30ce1 Match PyTaikoGreen gitattributes (add * -text) 2026-09-24 17:44:31 -04:00
Yonokid 828c9eb296 fix crashes 2026-09-24 14:50:23 -04:00
YonokidandClaude Sonnet 5 463194ccde Move fonts to dedicated Fonts folder
Keeps font.ttf and per-language font_*.ttf out of Graphics so
texture/atlas tooling doesn't need to skip non-image files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:15:58 -04:00
Yonokid 0b62466284 fix index bug 2026-09-22 10:46:42 -04:00
Yonokid c0ecb338ba remove get_id 2026-09-20 13:37:39 -04:00
Yonokid 047c3e9f3c remove sample animations 2026-09-19 18:27:30 -04:00
Yonokid 59c90c3ca1 move lots to lua 2026-09-19 13:45:48 -04:00
Yonokid 674b253bdd consolidate notes into one atlas 2026-09-18 13:17:33 -04:00
Yonokid 633f8b593a Merge pull request 'Result: the gold gauge cell follows the cleared state, not the cell count' (#37) from GanYiZhong/YataiDONNijiiro:result-gauge-clear into main
Reviewed-on: Yonokid/YataiDONNijiiro#37
2026-09-18 16:18:03 +02:00
Yonokid a5793af2ca Merge branch 'main' into result-gauge-clear 2026-09-18 16:17:52 +02:00
Yonokid 60c9178c86 Merge pull request 'i18n: Korean for every dictionary string' (#36) from GanYiZhong/YataiDONNijiiro:text-labels into main
Reviewed-on: Yonokid/YataiDONNijiiro#36
2026-09-18 16:17:43 +02:00
Yonokid 2994bdc94b Merge branch 'main' into text-labels 2026-09-18 16:17:31 +02:00
Yonokid 07092b1cac Merge pull request 'Lua: every user-visible string comes from skin_config (tex.skin_text)' (#35) from GanYiZhong/YataiDONNijiiro:lua-strings-to-config into main
Reviewed-on: Yonokid/YataiDONNijiiro#35
2026-09-18 16:17:00 +02:00
TaikoZhongandClaude Fable 5.1 ced2120784 result: the gold gauge cell follows the cleared state, not the cell count
The first gold cell was lit from the cell count (39 of 50 for oni) while the game clears on soul >= norma (8000); a failed run at 7800..7999 soul grew a gold cell. Draw the transition piece and the gold strip only when gauge_state >= 1, like the HUD gauge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 12:19:06 +08:00
TaikoZhongandClaude Fable 5.1 c5aeff2158 entry: the dan board's strings are mode_dan, not entry_dan
The engine puts a Dan-i Dojo board on the entry screen whenever the skin
defines an entry_dan text; this skin lists the dojo inside song select
instead (75f5d27), so the strings moved into skin_config must not use that
key.

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 01:45:48 +08:00
TaikoZhongandClaude Fable 5.1 113e246328 i18n: Korean for every dictionary string
Every skin_config text entry now carries ja / en / zh (Simplified) / ko.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 08:13:26 +08:00
Yonokid 7f6e883a09 Update checksums.sha256 2026-09-10 14:35:16 -04:00
Yonokid 58259f949d Merge branch 'GanYiZhong-entry-guide-title' 2026-09-10 13:39:38 -04:00
Yonokid f63d0901ba Merge pull request 'Song select: the decided genre board stays up while its songs load' (#22) from GanYiZhong/YataiDONNijiiro:folder-open-leg into main
Reviewed-on: Yonokid/YataiDONNijiiro#22
2026-09-10 19:32:35 +02:00
Yonokid 1b06d309f5 Merge branch 'main' into folder-open-leg 2026-09-10 19:32:18 +02:00
Yonokid ecc50e6c60 Merge pull request 'Light effects: entry glow patch size, add-blended layers drawn additively' (#21) from GanYiZhong/YataiDONNijiiro:light-effects into main
Reviewed-on: Yonokid/YataiDONNijiiro#21
2026-09-10 19:32:08 +02:00
TaikoZhongandClaude Fable 5.1 2b49aad1af Entry: control-guide mode per phase, mode board title centred on its band
- The top-left control guide follows the cabinet's ChangeMode: `decide` alone on
  the credit screen, the `all` loop (rim to choose + face to decide) on mode
  select when the list has more than one board, hidden during the drum select.
  entry.lua / entry/box.lua publish the phase, global/indicator.lua reads it.
- The open mode board's title sits in the middle of the dark band at the top of
  the plate; the text object's box is taller than its ink, so the nominal y is
  set for the ink (-118 -> -97).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 00:40:52 +08:00
TaikoZhongandClaude Fable 5.1 bffb67099c Result: the shinuchi SCORE caption is plain white too
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 00:38:33 +08:00
TaikoZhongandClaude Fable 5.1 c7acbf8527 Song select: a furled board is drawn as crisp columns, not a squashed bitmap
Squashing the 960-wide board texture to a few px averaged the outline away and,
because the GPU picks the mip level from the larger axis, blurred every row too;
the cabinet scales a vector shape and stays sharp.  draw_board now builds a
furled board from columns: both rounded corners at 1:1 and the band / panel /
band between them scaled in proportion, each drawn from an 8 px flat slice of
itself and magnified.  Narrower than both corners it becomes a solid bar with
the outline columns on either side and the flat centre between them (squeezed
corners left two hollow strokes with nothing between).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 00:37:30 +08:00
TaikoZhongandClaude Fable 5.1 d4b47f79d9 Song select: the folder close leg plays the cabinet's return half
Closing a folder (もどる) had no wheel motion at all: the fly table only
covered the open leg (clip f27..150), fly_offset only ran for kind "open", and
the engine's 166 ms fade took the song boxes and the back board out right
after the press -- the cabinet keeps them solid until they fly out (f184..204)
and the board furls (f205..217).

- anim/wheel_decide now spans f27..255 (open leg + `return` leg, straight from
  the cabinet clip); fly_offset plays the return half on the close leg: song
  boxes fly out, wait off screen across the swap (f219) and the genre boxes
  fly back in f234..254.  f255 re-bases the clip, so the leg stops before it.
- The engine's box fade is ignored during the pre-swap half of the close leg
  (box_fade); the clip channels already drive the back board's title fade and
  furl.

Verified with captures: press -> boxes stay -> contents fade / boxes fly out ->
back board furls -> genre board unfurls -> contents return -> boxes fly in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 18:35:47 +08:00
TaikoZhongandClaude Fable 5.1 7ce90d46b8 Song select: the decided genre board stays up while its songs load
Opening a folder on this engine differs from the cabinet in one way that
matters for the animation: the cabinet swaps the content 69 frames into the
decide leg, this engine swaps when its loader is done, which can be a couple
of seconds for a big folder.  The leg clock parked one frame short of the swap
(f113), i.e. with the decided board already furled away and the neighbours
flown out, so the screen sat empty for the whole load -- and before that the
engine re-grows the decided box's yellow frame, which made the board close
and reopen right after the press.

- The open leg now parks one frame short of the furl (f99): the decided board
  stays up, contents faded per the clip, as the loading state.  On the swap the
  clock resumes from the furl, so furl (f100..113) and the new board's unfurl
  (f114..) play back to back; a swap that lands before the park point just lets
  the clock run.
- The neighbours' fly-out (f86..99) plays before the swap as on the cabinet;
  it used to wait for the swap and then jump straight to the off-screen hold.
- The decided board is remembered at OPEN_BEGIN and drawn fully open for the
  whole leg, past the engine's closed re-grow and past the moment the engine
  takes the box off the wheel for the loader (draw_overlays bridges it from the
  still-live box handle until it has furled).
- The new content's song / back board is not drawn before the swap frame.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 07:26:09 +08:00
TaikoZhongandClaude Fable 5.1 037a9c5816 Result: the スコア / SCORE caption is plain white on the cabinet
The caption textures carried a heavy dark outline; the cabinet's board header
shows the caption in plain white.  Re-rendered with the skin font, white fill,
no outline, same size and position as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 06:50:12 +08:00
TaikoZhongandClaude Fable 5.1 96f0f6f7aa Result: centre the スコア caption on the board's header band
The caption sat at y 281, which put its glyphs at 290..321 on a header band
that spans 284..314, so the bottom of the text spilled onto the body of the
board.  At y 274 the glyphs sit at 283..314, centred on the band.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 06:21:18 +08:00
TaikoZhongandClaude Fable 5.1 462e4602c6 Fever backdrops: export the steady state, not the transition's parked copies
The fever movies' root timeline is a state machine: frame 0 (`init`) parks the
fly-in copies of the transition -- a second sky plate, the wave scrollers, the
boats -- at alpha 0, and the steady state is the `fever` label, where only
fever_mc remains.  The export took the root's frame-0 children and forced the
parked ones visible, so the third backdrop drew its sky and its big waves twice
(one row over the other) and the boats and Mt Fuji ended up behind an extra wave
layer.  movie_export now takes the root's children at the `fever` label and only
the ones shown there; all three fever backdrops re-exported.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 06:10:36 +08:00
TaikoZhongandClaude Fable 5.1 11417f080f Fever backdrop 3: stop painting the movie's mask plate
bg_fever_a_03 places a full-stage plate at fever_mc depth 4 with a clip depth:
it is a mask that only clips the layer above it and is never painted, but the
export drew it as an ordinary leaf, which put a flat green sheet over the sky.
movie_export now skips placements that carry a clip depth; re-exported, the
plate and its texture are the only change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 03:34:02 +08:00
TaikoZhongandClaude Fable 5.1 0985512f91 Entry: bake the warm glows as two non-overlapping patches
The four pre-composited patches overlapped (2 and 3 by 31 px, and after the
size fix 0 and 2 as well), and where they overlapped the later patch replaced
the earlier one's glow instead of adding to it, which showed as a vertical dark
band between the lamps.  The glows are now summed onto the backdrop once and
cut into two boxes that do not touch: the left lamp, and the three right-hand
lamps together.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 03:30:55 +08:00
TaikoZhong 3a815102a1 Update checksums.sha256 for the changed textures and scripts 2026-09-10 03:30:03 +08:00
TaikoZhongandClaude Fable 5.1 5ee758f90f Replay the cabinet's add-blended light effects as additive
The exported clips and backdrop movies were drawn source-over everywhere, but the
cabinet places a number of glow layers with its add blend mode: the lantern glows
of the lower backdrops (bg_nomal_a_01..05), the crown's radial burst, the score
rank plate's effect copy, and the full-combo / donderful letter glows.  Drawn
source-over, those textures show up as dark or grey patches.

- movie_export / clip_export record the placement's blend mode (`blend = 8` on a
  backdrop leaf, an 8th row value on a clip leaf; the root placement's mode is
  folded into its group), and bg_anim / clip_player / ending draw such leaves
  with blend = "additive".  Data re-exported; only the blend field changed.
- result_crown: the burst copies draw additively; the crown_loop shine sweep is a
  plain white polygon on the cabinet, so gleam frames 0..2 are white now instead
  of the atlas colour they were exported with.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 01:49:19 +08:00
TaikoZhongandClaude Fable 5.1 e1b6f78cb9 Entry: give the four warm-glow patches one common size
The engine sizes every frame of a folder texture like frame 0 (source rect and
default x2/y2), so the 177x311 and 233x233 patches were being stretched to
216x311 and their edges showed as boxes around the lamps.  Each patch is now
padded to 233x311 with the untouched backdrop around it; the patch is bg + glow,
so the extra backdrop is invisible at any fade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 01:49:17 +08:00
Yonokid 5ab735c76d Update checksums.sha256 2026-09-09 12:07:03 -04:00
Yonokid 089ee6d7fc Merge pull request 'Song select: the course mark pops to its decided look once the course is locked in' (#20) from GanYiZhong/YataiDONNijiiro:diff-mark-decide into main
Reviewed-on: Yonokid/YataiDONNijiiro#20
2026-09-09 17:35:33 +02:00
Yonokid 26419b4b1c Merge branch 'main' into diff-mark-decide 2026-09-09 17:35:23 +02:00
Yonokid 809c81957d Merge pull request 'Score rank (粋 / 雅 / 極): in-game badge and result plate' (#19) from GanYiZhong/YataiDONNijiiro:score-rank-display into main
Reviewed-on: Yonokid/YataiDONNijiiro#19
2026-09-09 17:35:14 +02:00
Yonokid 6e2e97d6fe Merge branch 'main' into score-rank-display 2026-09-09 17:33:26 +02:00
Yonokid c227ea3ce6 Merge pull request 'Ending banners: replay the original clips (clear / full combo / donderful / fail)' (#18) from GanYiZhong/YataiDONNijiiro:ending-clips into main
Reviewed-on: Yonokid/YataiDONNijiiro#18
2026-09-09 17:25:58 +02:00
Yonokid c7c69dc3fe Merge pull request 'Entry: draw the mode-select cursor glow under the board' (#17) from GanYiZhong/YataiDONNijiiro:entry-mode-cursor-under into main
Reviewed-on: Yonokid/YataiDONNijiiro#17
2026-09-09 17:25:26 +02:00
Yonokid 5898df4231 Merge branch 'main' into entry-mode-cursor-under 2026-09-09 17:22:51 +02:00
Yonokid ec5e73019b Merge pull request 'Lower backdrops: replay every element of the original movies' (#16) from GanYiZhong/YataiDONNijiiro:lower-bg-anim into main
Reviewed-on: Yonokid/YataiDONNijiiro#16
2026-09-09 17:21:58 +02:00
Yonokid 2633ff421b Merge branch 'main' into lower-bg-anim 2026-09-09 17:20:37 +02:00
Yonokid e1ec601b27 Merge pull request 'Song select: no credit line, course names at the record's size' (#15) from GanYiZhong/YataiDONNijiiro:songselect-tweaks into main
Reviewed-on: Yonokid/YataiDONNijiiro#15
2026-09-09 17:20:05 +02:00
Yonokid 3c1f9faad5 Merge branch 'main' into songselect-tweaks 2026-09-09 17:19:13 +02:00
Yonokid e0c33eb940 Merge pull request 'Gauge: centre the soul-max fire where the original puts it' (#14) from GanYiZhong/YataiDONNijiiro:gauge-fire-position into main
Reviewed-on: Yonokid/YataiDONNijiiro#14
2026-09-09 17:19:03 +02:00
Yonokid aa4967d5a3 Merge branch 'main' into gauge-fire-position 2026-09-09 17:16:28 +02:00
Yonokid 78dc1281ae Merge pull request 'donbg band: replay every element of the original movie' (#11) from GanYiZhong/YataiDONNijiiro:donbg-anim into main
Reviewed-on: Yonokid/YataiDONNijiiro#11
2026-09-09 17:16:17 +02:00
Yonokid e2350bc9bc Merge branch 'main' into donbg-anim 2026-09-09 17:15:27 +02:00
Yonokid dfda066441 Merge pull request 'Result: 2P backdrop is red on the left and cyan on the right' (#13) from GanYiZhong/YataiDONNijiiro:result-2p-colors into main
Reviewed-on: Yonokid/YataiDONNijiiro#13
2026-09-09 17:15:06 +02:00
Yonokid cd4f675ed1 Merge branch 'main' into result-2p-colors 2026-09-09 17:13:27 +02:00
Yonokid 8ff6e2402d Merge pull request 'Roll effect: fly over the lower backdrop along the original paths' (#12) from GanYiZhong/YataiDONNijiiro:renda-lower-bg into main
Reviewed-on: Yonokid/YataiDONNijiiro#12
2026-09-09 17:13:14 +02:00
Yonokid 4184dcbb15 Merge branch 'main' into renda-lower-bg 2026-09-09 17:12:09 +02:00
Yonokid 0c606013c2 Merge branch 'main' into donbg-anim 2026-09-09 17:10:52 +02:00
Yonokid ef20f18926 Merge pull request 'Soul-max crowd: replay the per-group movie instead of a flat overlay' (#10) from GanYiZhong/YataiDONNijiiro:fever-crowd-anim into main
Reviewed-on: Yonokid/YataiDONNijiiro#10
2026-09-09 17:10:35 +02:00
Yonokid 9c7111de17 Merge branch 'main' into fever-crowd-anim 2026-09-09 17:09:06 +02:00
Yonokid bb0bb1cf28 Merge pull request 'dan doors: tear down outside SONG_SELECT / DAN_SELECT' (#9) from GanYiZhong/YataiDONNijiiro:dan-doors-teardown into main
Reviewed-on: Yonokid/YataiDONNijiiro#9
2026-09-09 17:08:54 +02:00
Yonokid 10d00c1799 Merge branch 'main' into dan-doors-teardown 2026-09-09 17:08:05 +02:00
Yonokid 0ba0cc1a38 Merge branch 'GanYiZhong-hud-nijiiro-modifiers' 2026-09-09 11:07:43 -04:00
Yonokid aa09e56cfa Merge pull request 'entry: centre the credit-row text, white outline on the 1P/2P labels' (#7) from GanYiZhong/YataiDONNijiiro:entry-credit-text into main
Reviewed-on: Yonokid/YataiDONNijiiro#7
2026-09-09 17:06:08 +02:00
Yonokid 24443674a4 Merge pull request 'song select: drop the placeholder NEIRO frame under the return button' (#6) from GanYiZhong/YataiDONNijiiro:diffselect-no-neiro-frame into main
Reviewed-on: Yonokid/YataiDONNijiiro#6
2026-09-09 17:05:55 +02:00
Yonokid 45219c8f6b Merge branch 'main' into diffselect-no-neiro-frame 2026-09-09 17:05:36 +02:00
Yonokid 1d6b956b69 Merge pull request 'entry: drop the 「つかわない」 caption' (#5) from GanYiZhong/YataiDONNijiiro:entry-no-cancel-caption into main
Reviewed-on: Yonokid/YataiDONNijiiro#5
2026-09-09 17:05:27 +02:00
Yonokid 5e96ba9c02 Merge branch 'main' into entry-no-cancel-caption 2026-09-09 17:05:06 +02:00
Yonokid c24c3650bc Merge branch 'GanYiZhong-fix-gauge-fade' 2026-09-09 11:04:40 -04:00
TaikoZhongandClaude Fable 5.1 188edce33b Song select: the course mark pops to its decided look once the course is locked in
main_diff_mark 'course_decide_start' (frames 35..64): once player:difficulty_decided()
(engine PR #162) the translucent 680x680 mark behind the difficulty board goes opaque
over a white backing, stretching to 1.235x its height and back while a white copy
flashes on top, then holds the 'course_decide' look.  Opaque shapes of mark_course
(course_1..5) and mark_course_white exported to Graphics/song_select/diff_mark_anim.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 16:09:11 +08:00
TaikoZhongandClaude Fable 5.1 558693399a Result: play the score-rank sound with the rank plate
The original plays se_result `scorerank_NN_<seat>` (one sample for every rank)
when the plate appears; shipped as scorerank_{c,l,r} and played when the
ScoreRank state starts, seat chosen like the crown sound.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:26:32 +08:00
TaikoZhongandClaude Fable 5.1 ecdb84701b Score rank badge: one clip, restarted per rank line
The original only ever plays `in`; the clip then keeps and fades out by itself,
and a new rank line restarts it. Exported as one 162-frame clip instead of the
in/out pair, so the plate fades out after its keep span as on the cabinet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:12:23 +08:00
TaikoZhongandClaude Fable 5.1 6b9dafeff2 Soul-max crowd: step at the song's beat like the original
The original re-times this movie to the song (the same frame sync the dancers
use): the 40-frame bob is one beat, so 60 fps only at 90 BPM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:12:14 +08:00
TaikoZhongandClaude Fable 5.1 5947408c67 Score rank (粋 / 雅 / 極): in-game badge and result plate
- In game: the rank plate slides in at the player panel's top-left as the score
  crosses each rank line (500k white, 600k bronze, 700k silver, 800k gold,
  900k pink, 950k purple, 1M rainbow), swapping through the out/in pair on the
  next line. Uses the engine's Background:handle_score hook.
- Result: a ScoreRank state between the score count-up and the crown, playing
  the original's plate clip (極 with its own burst) under the score box.
- Song select already draws the chips from the stored rank.
Clips exported from the original with the rank plates as swappable shapes;
Scripts/game/clip_player.lua is the shared frame player.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 14:44:37 +08:00
TaikoZhongandClaude Fable 5.1 5d35943d3e Ending banners: replay the original clips (clear / full combo / donderful / fail)
Uses the engine's new Background:handle_ending / draw_ending hook. The four
ending clips are exported frame by frame (Scripts/game/ending_anim, textures
under game/ending_<kind>_anim) and replayed on the lane at 60 fps: the banner
text reveal, the fans, the two dancers and the confetti all move as in the
original, then loop. Without the engine hook the skin's static plates still
draw as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 14:07:48 +08:00
TaikoZhongandClaude Fable 5.1 14bbae37cf Entry: draw the mode-select cursor glow under the board
The original stacks the cursor glow below the board, so the characters that
lean out of the board stand in front of it; ours drew the glow on top and
covered them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 13:46:32 +08:00
TaikoZhongandClaude Fable 5.1 a87e8921d8 Soul-max crowd: step at 60 Hz
Checked against the original: the crowd bobs at half the speed the 120 Hz
step gave it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 13:37:18 +08:00
Yonokid e78bd361e1 Merge pull request 'Enable gauge_cell_fade_in' (#3) from GanYiZhong/YataiDONNijiiro:sco-gauge-cell-fade-in into main
Reviewed-on: Yonokid/YataiDONNijiiro#3
2026-09-09 06:32:40 +02:00
Yonokid 24c1d48543 Merge branch 'main' into sco-gauge-cell-fade-in 2026-09-09 06:32:29 +02:00
TaikoZhongandClaude Fable 5.1 60add28047 Lower backdrops: replay every element of the original movies
The five normal and three cleared backdrops were flat plates plus a few hand
rebuilt effects, so the koi, the treasure ships, the lantern glow and the
festival stalls never moved as they do in the original. Each movie is now
exported per shape instance with its own loop (bg_data/bg_anim/<name>.lua,
textures under background/bga_<name>) and replayed by bg_objects/bg_anim.lua at
60 fps on the lower half of the screen. bg_normal.lua and bg_fever.lua use it
when the data exists and keep their old drawing as the fallback; the cleared
backdrop's rise/fall wipe is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:28:29 +08:00
Yonokid 319603acb7 Merge pull request 'Enable combo_color_tiers (silver 50+, gold 100+)' (#2) from GanYiZhong/YataiDONNijiiro:sco-combo-color-tiers into main
Reviewed-on: Yonokid/YataiDONNijiiro#2
2026-09-09 06:23:22 +02:00
TaikoZhongandClaude Fable 5.1 f69e411380 Song select: no credit line, course names at the record's size
The cabinet's song select shows no COINS / FREE PLAY line (RESULT keeps it),
and the course names on the difficulty cards are drawn at the record's size 34
instead of the 28 an earlier crop estimate picked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:19:21 +08:00
TaikoZhongandClaude Fable 5.1 85e210f038 Gauge: centre the soul-max fire where the original puts it
The fire sat 24 px right and 52 px above the 魂 mark's centre; the original
places it 43 px right and 43 px above. Anchors moved accordingly for both lanes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:11:57 +08:00
TaikoZhongandClaude Fable 5.1 8874f72738 donbg band: map shape characters to their real geometry records
A shape character can own more than one geometry record and records exist for
characters no frame places, so ranking the character id among the placed
shapes shifted textures after the first such case: band 04's cleared tile came
out 408 px wide with gaps and every later shape drew the wrong art (a temari
where a cloud belongs). The exporter now reads the character-to-record map from
the movie and composites multi-record characters into one texture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:02:01 +08:00
TaikoZhongandClaude Fable 5.1 a0c9513f43 Result: 2P backdrop is red on the left and cyan on the right
The double-play result plate was baked from the wrong atlas layer, so it came
out orange / teal with the header band burnt in. Rebuilt from the original's
two sky halves (the left one placed mirrored) and the header band is now drawn
by the layer code, the same way the 1P backdrop draws it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:01:49 +08:00
TaikoZhongandClaude Fable 5.1 ea2b88cac9 Roll effect: 60 Hz and a pool of ten
Checked against the original: the pieces cross at half the speed the 120 Hz
step gave them, and only about ten are ever live -- the oldest is replaced as
soon as an eleventh starts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:35:53 +08:00
TaikoZhongandClaude Fable 5.1 d8eb4ec30e donbg band: back to 60 Hz
Checked against the original: the band runs at half the speed the 120 Hz step
gave it. Only the soul-max crowd steps at 120 Hz.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:35:47 +08:00
TaikoZhongandClaude Fable 5.1 05c8bb27f5 Roll effect: fly over the lower backdrop along the original paths
The birds / shuriken / arrows spawned in the top-left corner and swept across
the top band. The original spawns them over the lower backdrop (x 6..1100,
y 540..1020), behind the dancers, and each movie has its own path: birds and
arrows rise to the right, shuriken fly level and spin. Replayed at 120 Hz for
120 frames, same 25-slot pool as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:26:55 +08:00
TaikoZhongandClaude Fable 5.1 9f341ac787 donbg band: export the cleared-state group with its real alphas
The root timeline parks the cleared-state group at alpha 0 until the gauge
clears, so exporting through the root left every cleared band invisible and the
not-cleared art stayed up after クリア. Each state group is now exported as its
own root and offset by its placement; all twelve bands carry both states.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:11:57 +08:00
TaikoZhongandClaude Fable 5.1 f19a8ae735 donbg band: step the movie at 120 Hz like the original
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:05:14 +08:00
TaikoZhongandClaude Fable 5.1 561a1ef5c4 Soul-max crowd: step at 120 Hz and paint it over the band
The original advances the movie at 120 Hz, so the bob ran at half speed, and it
paints the crowd after the bottom band, not under it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:05:07 +08:00
TaikoZhongandClaude Fable 5.1 a0e9ca66a6 Soul-max crowd: draw from the two crowds the original actually uses
The original's random pool only holds fever_effect1 and fever_effect2;
fever_effect0 is a single figure parked off-screen, so picking it showed an
empty crowd at 魂 max. Drop it and pick from the two real crowds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 10:55:21 +08:00
TaikoZhongandClaude Fable 5.1 f50ee6631d donbg band: replay every element of the original movie
The band behind the player used to be a scrolling tile plus a few static plates,
so the squids, waves, fans, fish and birds never moved the way they do in the
original. Each of the twelve bands (six movies x 1P/2P) is now data-driven: one
texture per shape (background/donbga_NN_Mp) and, per shape instance, the period
of its own loop with per-frame position/scale/rotation/alpha
(bg_data/donbg_anim/a_NN_Mp.lua); flipbook frames carry a separate presence mask so
the fish's arc and its frame swap keep their own periods. donbg/anim_player.lua
replays all of it on one 60 fps clock; base.lua uses it when the data exists and
keeps the old layer lists as the fallback (collab bands are unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 10:12:43 +08:00
TaikoZhongandClaude Fable 5.1 d459a0f285 Soul-max crowd: replay the per-group movie instead of a flat overlay
The 魂 max crowd was two flattened PNGs, so every figure sat at one fixed
position, the turtle/shiba groups lost their depth order, and nothing moved.
The crowd is now data-driven: one texture per crowd group
(background/fevera_N) plus a per-frame display list
(bg_data/fever_anim/f_N.lua) covering the rise, the loop and the fall, replayed
by bg_objects/fever_anim.lua at 60 fps. Every group bobs and squashes on its
own phase and layers exactly as authored; the base of the crowd lands on the
band. Three crowds are shipped; the old flat overlays stay as a fallback for
skins without the data.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 09:48:44 +08:00
TaikoZhongandClaude Fable 5.1 8ce99e2679 dan doors: tear the rig down when the screen is not SONG_SELECT/DAN_SELECT
Backing out of DAN_SELECT straight to TITLE skipped the song-select teardown,
so the shut doors stayed painted over the title screen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 08:48:57 +08:00
TaikoZhongandClaude Fable 5.1 94eb92d8c2 song select: per-value speed badges in the option panel
The はやさ row showed the same badge for every value; use the cabinet's
one-badge-per-speed set (x1.1 .. x1.9, x2, x2.5, x3, x3.5, x4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 08:30:29 +08:00
TaikoZhongandClaude Fable 5.1 c64d9874f2 game HUD: cabinet option badges on the 3x2 slot grid
The in-game modifier badges were inherited from the base skin (2x2, three
coarse speed tiers). Ship the cabinet badge art (one per speed x1.1..x4,
doron, abekobe, kimagure, detarame) and declare the cabinet slot grid
(3 columns x 2 rows, 44 px pitch, first slot at 170,77) as mod_badge_grid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 07:34:58 +08:00
TaikoZhongandClaude Fable 5.1 2d18433adf entry: centre the credit-row text and give the 1P/2P labels their white outline
The 1人プレイ / 2人プレイ labels and the 太鼓をたたいてスタート! prompt sat about
10 px above the middle of their yellow pills; the labels also lacked the
cabinet's white outline (they were drawn black on black).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 06:25:28 +08:00
TaikoZhongandClaude Fable 5.1 97daf61279 gauge: draw the fill on the cabinet's 50-cell / 21 px grid
The gauge art (border/overlay separators, クリア box, 魂) is a 50-cell, 21 px
grid from x=738, but the fill pieces were 12 px wide and the engine stepped
in 87 units, so the fill edge landed mid-cell ("超過但不到一格"). Declare
gauge_cells = 50 (engine reads it via skin_entry) and widen every fill
piece to one 21 px cell; the fade/transition pieces are anchored on the
cell they belong to and lined up with the bar rows (1P -34 / 2P -91).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 04:46:42 +08:00
TaikoZhongandClaude Fable 5.1 3640b8963b song select: drop the placeholder NEIRO frame under the return button
The course-select board drew a faint 80 px square at (440, 602) as a stand-in
for a separate NEIRO slot; the cabinet has no such element and it read as a
stray popup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 02:05:53 +08:00
TaikoZhongandClaude Fable 5.1 4de1d53fb2 entry: drop the 「つかわない」 caption
The cabinet's entry screen has no such caption; the cancel side is simply
the un-armed state, so the label sat as stray text under the prompts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 23:04:54 +08:00
TaikoZhongandClaude Fable 5.1 f0d3de2316 Enable gauge_cell_fade_in (new gauge cell fades in instead of flashing)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 21:22:41 +08:00
TaikoZhongandClaude Fable 5.1 dccba9f62d gauge: align the cell fade-in sprite with the bar
1p/2p_bar_fade was a 24 px (two-cell) sprite drawn half a cell left of the
new cell and 3 px below the bar (4 px for 2P), so gaining a cell looked like
a half-cell bump. Crop it to one cell (12 px), anchor it on the new cell and
line its body up with the bar rows. Gauge frame position is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 20:29:19 +08:00
TaikoZhongandClaude Fable 5.1 1153af1511 Enable combo_color_tiers (silver 50+, gold 100+)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 17:02:04 +08:00
Yonokid 53a0a99e2f fix slop graphic 2026-09-04 15:34:45 -04:00
Yonokid 6067141adf minor fixes 2026-09-04 13:20:24 -04:00
Yonokid fe649c98ff compress videos 2026-09-04 11:44:33 -04:00
Yonokid 19ad8e43e3 enable dan folders in song select 2026-09-02 16:15:22 -04:00
Yonokid 8a02b53d66 Update checksums.sha256 2026-09-02 16:07:06 -04:00
Yonokid ac550dbfa1 delete settings page 2026-09-02 15:34:24 -04:00
Yonokid f44e0493e6 update checksums 2026-09-02 14:51:46 -04:00
Yonokid 55b11fdb8a first commit 2026-09-02 14:25:04 -04:00
101 changed files with 2219 additions and 2283 deletions

No files matched your search

+1
View File
@@ -3,3 +3,4 @@
*.ogg filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
{
"page1": {
"x": 0,
"y": 0
},
"page2": {
"x": 0,
"y": 0
}
}
+57
View File
@@ -164,5 +164,62 @@
}
],
"comment": "ROUND 17: copied verbatim from song_select/animation.json so the 演奏オプション panel the 段位道場 confirmation dialog's option chip opens runs identically on DAN_SELECT. ModifierSelector's constructor reads 28/29/30/31/32 (+39 optional) and get_animation THROWS on a missing id; a child animation.json replaces the parent's rather than merging, so DAN_SELECT had none of them. option/neiro panel slide OUT (ModifierSelector/NeiroSelector pick this up on confirm when it exists; without it they replay id 28). Arcade MC65 label 'out' = frames 25..44: y 0 -> -75 over 4 frames, then -75 -> 650 over 15. Engine space the drawn y is `attribute + song_select_offset.x` (= -548 in this skin), so attribute runs 0 -> -75 at 4/19 = 0.2105 -> 548."
},
{
"id": 19,
"type": "move",
"duration": 450,
"total_distance": 1920,
"ease_in": "quadratic",
"comment": "dan transition slide in (PyTaikoGreen x1.5)"
},
{
"id": 20,
"type": "move",
"duration": 383,
"delay": 517,
"total_distance": -705,
"waypoints": [
{
"at": 200,
"value": -768,
"ease_in": "quadratic"
},
{
"at": 283,
"value": -689
}
],
"comment": "dan transition don rises from the bottom, overshoots and settles (PyTaikoGreen x1.5)"
},
{
"id": 21,
"type": "fade",
"duration": 400,
"delay": 483,
"initial_opacity": 0.0,
"final_opacity": 1.0,
"ease_out": "quadratic",
"comment": "dan transition emblem fade in"
},
{
"id": 22,
"type": "move",
"duration": 500,
"delay": 983,
"start_position": 0,
"total_distance": 90,
"ease_in": "quadratic",
"comment": "dan transition emblem spin (degrees, not scaled)"
},
{
"id": 23,
"type": "fade",
"duration": 483,
"delay": 1000,
"initial_opacity": 0.13,
"final_opacity": 1.0,
"ease_out": "quadratic",
"comment": "dan transition white flash; its end is the end of the transition"
}
]
+148
View File
@@ -0,0 +1,148 @@
[
{
"id": 0,
"type": "fade",
"duration": 100,
"initial_opacity": 0.0,
"final_opacity": 1.0,
"delay": 20,
"lock_input": true,
"comment": "fade in the side select box"
},
{
"id": 1,
"type": "fade",
"duration": 500,
"initial_opacity": 0.5,
"final_opacity": 0.4,
"reverse_delay": 0,
"loop": true,
"comment": "background lights flicker"
},
{
"id": 2,
"type": "move",
"duration": 550,
"total_distance": -245,
"ease_in": "quadratic",
"lock_input": true,
"waypoints": [
{ "at": 350, "value": -295, "ease_out": "quadratic" }
],
"comment": "move donchan's drum up/down a little (merged from old ids 2+3)"
},
{
"id": 4,
"type": "move",
"duration": 350,
"total_distance": -170,
"delay": {
"reference_id": 2,
"property": "duration"
},
"ease_out": "quadratic",
"lock_input": true,
"comment": "move donchan's drum to the left/right"
},
{
"id": 5,
"type": "texture_resize",
"duration": 350,
"initial_size": 0.75,
"final_size": 1.0,
"comment": "initial cloud resize, larger than loop"
},
{
"id": 6,
"type": "texture_resize",
"duration": 200,
"initial_size": 1.0,
"final_size": 1.2,
"reverse_delay": 200,
"loop": true,
"delay": {
"reference_id": 5,
"property": "duration"
},
"comment": "loop resize donchan cloud back and forth"
},
{
"id": 7,
"type": "texture_change",
"duration": 333,
"delay": {
"init_val": {
"reference_id": 4,
"property": "duration"
},
"reference_id": 4,
"property": "delay"
},
"textures": [
[0, 83.35, 0],
[83.35, 166.7, 1],
[166.7, 250, 2],
[250, 333, 3]
],
"comment": "cloud keyframes"
},
{
"id": 8,
"type": "fade",
"duration": 83.35,
"delay": {
"init_val": {
"reference_id": 7,
"property": "delay"
},
"reference_id": 7,
"property": "duration"
},
"comment": "fade out the cloud"
},
{
"id": 9,
"type": "fade",
"duration": 160,
"lock_input": true,
"comment": "fade out the mode select boxes"
},
{
"id": 10,
"type": "move",
"duration": 166.67,
"total_distance": 50,
"comment": "box movement"
},
{
"id": 11,
"type": "move",
"duration": 133.34,
"total_distance": 54,
"delay": 200,
"comment": "box opening"
},
{
"id": 12,
"type": "fade",
"duration": 200,
"initial_opacity": 0.0,
"final_opacity": 1.0,
"comment": "nameplate fade in"
},
{
"id": 13,
"type": "fade",
"duration": 1000,
"loop": true,
"comment": "costume menu blue arrow fade"
},
{
"id": 14,
"type": "move",
"duration": 1000,
"total_distance": 25,
"loop": true,
"comment": "costume menu blue arrow move"
}
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
{
"player_entry_en": {
"x": 0,
"y": 0
},
"player_entry_ja": {
"x": 0,
"y": 0
}
}
+18 -1
View File
@@ -1186,5 +1186,22 @@
],
"loop": true,
"comment": "r57: result-gauge tamashii fire cels (replaces the id-20 kusudama-FADE collision; body = id 25, the real 8-cel 400 ms fire cycle)"
}
},
{
"id": 76,
"type": "texture_resize",
"duration": 133.36,
"initial_size": 0.0,
"final_size": 1.0,
"comment": "gogotime lane effect expands from center over 8 frames (16.67ms each)"
},
{
"id": 77,
"type": "fade",
"duration": 283.3,
"initial_opacity": 1.0,
"final_opacity": 0.0,
"delay": 0,
"comment": "judgment fade 3"
}
]
Binary file not shown.
+7 -1
View File
@@ -260,5 +260,11 @@
440
]
]
},
"lane_effect": {
"x": 496,
"y": 12,
"x2": 1424,
"y2": 195
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -641,6 +641,9 @@
"x": 0,
"y": 110
},
"yellow_box_bottom": {
"y": 974
},
"dan_level_counter_margin": {
"x": 34
},
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,244 +0,0 @@
{
"background": {
"x": -6008,
"y": 433
},
"background_2": {
"x": -6001,
"y": 247
},
"title": [
{
"x": -5933,
"y": 442
},
{
"x": -5933,
"y": 268
}
],
"normal": [
{
"x": -5977,
"y": 491
},
{
"x": -5977,
"y": 317
}
],
"normal_ura": [
{
"x": -5977,
"y": 491
},
{
"x": -5977,
"y": 317
}
],
"shinuchi": [
{
"x": -5977,
"y": 542
},
{
"x": -5977,
"y": 317
}
],
"shinuchi_ura": [
{
"x": -5977,
"y": 542
},
{
"x": -5977,
"y": 317
}
],
"pts": [
{
"x": -5795,
"y": 495
},
{
"x": -5808,
"y": 321
}
],
"counter": [
{
"x": -5850,
"y": 495,
"crop": [
[
0,
0,
16,
32
],
[
16,
0,
16,
32
],
[
32,
0,
16,
32
],
[
48,
0,
16,
32
],
[
64,
0,
16,
32
],
[
80,
0,
16,
32
],
[
96,
0,
16,
32
],
[
112,
0,
16,
32
],
[
128,
0,
16,
32
],
[
144,
0,
16,
32
]
]
},
{
"x": -5850,
"y": 321
}
],
"difficulty": [
{
"x": -5942,
"y": 495
},
{
"x": -5772,
"y": 323
}
],
"ura": [
{
"x": -5974,
"y": 495
},
{
"x": -5974,
"y": 321
}
],
"judge_good": {
"x": -5974,
"y": 368
},
"judge_ok": {
"x": -5974,
"y": 418
},
"judge_bad": {
"x": -5974,
"y": 468
},
"judge_drumroll": {
"x": -6036,
"y": 518
},
"judge_num": {
"x": -5752,
"y": 318,
"crop": [
[
0,
0,
32,
40
],
[
32,
0,
32,
40
],
[
64,
0,
32,
40
],
[
96,
0,
32,
40
],
[
128,
0,
32,
40
],
[
160,
0,
32,
40
],
[
192,
0,
32,
40
],
[
224,
0,
32,
40
],
[
256,
0,
32,
40
],
[
288,
0,
32,
40
]
]
}
}
Binary file not shown.
Binary file not shown.
+7 -2
View File
@@ -105,8 +105,6 @@ function Background.new(player_num, bpm, scene_preset)
self.scene_preset = scene_preset
self.is_2p = (player_num == 3) -- PlayerNum::TWO_PLAYER
tex.load_animations("background")
-- child-only: arcade soul-gauge overlay, drawn in draw_fore (in front of the HUD)
self.arcade_gauge = ArcadeGauge.new()
@@ -158,6 +156,13 @@ function Background.new(player_num, bpm, scene_preset)
return self
end
-- a miss streak darkens the donbg band (each player's own band in 2P)
function Background:handle_miss_streak(player_num, on)
if self.collab then return end
local d = self.is_2p and (player_num == 2 and self.donbg_2 or self.donbg_1) or self.donbg
if d then d:set_dark(on) end
end
function Background:update(current_ms, bpm)
self.bpm = bpm
self.enso_skip:update(current_ms)
+3 -3
View File
@@ -40,7 +40,7 @@ function A3BGNormal.new()
local self = setmetatable({}, A3BGNormal)
self.name = "bg_0"
tex.load_folder(COLLAB_PATH, "bg_normal/" .. self.name)
self.flicker = tex.get_animation(9)
self.flicker = tex.get_animation(9, "background")
self._id_bg = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/background") or 0
self._id_overlay = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/overlay") or 0
self._id_curtain = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/curtain") or 0
@@ -70,8 +70,8 @@ function A3BGFever.new()
self.name = "bg_fever_0"
tex.load_folder(COLLAB_PATH, "bg_fever/" .. self.name)
self.transitioned = false
self.horizontal_move = tex.get_animation(16)
self.bg_texture_move = tex.get_animation(17)
self.horizontal_move = tex.get_animation(16, "background")
self.bg_texture_move = tex.get_animation(17, "background")
local petal_info = tex.get_texture_info(self.name, "petals")
self.petal_width = petal_info and petal_info.x2[1] or 0
self.vertical_move = anim.move(1300, { total_distance = self.petal_width, loop = true })
+1 -1
View File
@@ -15,7 +15,7 @@ function DanDonBG.new(player_num)
local self = setmetatable({}, DanDonBG)
local base = DonBGBase.new(1, player_num, COLLAB_PATH)
for k, v in pairs(base) do self[k] = v end
self.overlay_move = tex.get_animation(2)
self.overlay_move = tex.get_animation(2, "background")
local bg_overlay_info = tex.get_texture_info(self.name, "bg_overlay")
self.bg_overlay_width = bg_overlay_info and bg_overlay_info.x2[1] or 0
local bg_overlay_2_info = tex.get_texture_info(self.name, "bg_overlay_2")
+2 -2
View File
@@ -20,8 +20,8 @@ function FunassyiBGFever.new()
self.name = "bg_fever_0"
tex.load_folder(COLLAB_PATH, "bg_fever/" .. self.name)
self.transitioned = false
self.horizontal_move = tex.get_animation(16)
self.bg_texture_move = tex.get_animation(17)
self.horizontal_move = tex.get_animation(16, "background")
self.bg_texture_move = tex.get_animation(17, "background")
local overlay_info = tex.get_texture_info(self.name, "overlay")
self.overlay_width = overlay_info and overlay_info.x2[1] or 0
self._id_bg = tex.get_texture(COLLAB_PATH .. "/" .. self.name .. "/background") or 0
+1 -1
View File
@@ -19,7 +19,7 @@ function GumiDonBG.new(index, player_num, path)
for k, v in pairs(base) do self[k] = v end
-- this skin's donbg/6 is its own (a static band) and leaves out what PyTaikoGreen's DonBG6
-- sets for the scrolling overlays that draw_textures below uses
self.overlay_move = self.overlay_move or tex.get_animation(2)
self.overlay_move = self.overlay_move or tex.get_animation(2, "background")
self.overlay_1_texture = self.overlay_1_texture or tex.get_texture_info(self.name, "overlay_1")
self.overlay_2_texture = self.overlay_2_texture or tex.get_texture_info(self.name, "overlay_2")
self._id_overlay_1 = self._id_overlay_1 or tex.get_texture(self.path .. "/" .. self.name .. "/" .. "overlay_1") or 0
+1 -1
View File
@@ -13,7 +13,7 @@ function TutorialDonBG.new(index, player_num, path)
end
self.move = anim.move(6000, { total_distance = -self.bg_texture.x2[1], loop = true })
self.move:start()
self.overlay_move = tex.get_animation(2)
self.overlay_move = tex.get_animation(2, "background")
self.overlay_texture = tex.get_texture_info(self.name, "overlay")
self.footer_texture = tex.get_texture_info(self.name, "footer")
self._id_overlay = tex.get_texture(self.path .. "/" .. self.name .. "/overlay") or 0
@@ -9,9 +9,6 @@ function ArcadeGauge.new()
local self = setmetatable({}, ArcadeGauge)
self.p = {}
tex.unload_folder("result", "gauge")
tex.unload_folder("game", "gauge")
return self
end
+5 -5
View File
@@ -29,7 +29,7 @@ function BGNormal01.new(index, path)
local self = setmetatable({}, BGNormal01)
local base = BGNormalBase.new(index, path)
for k, v in pairs(base) do self[k] = v end
self.flicker = tex.get_animation(9)
self.flicker = tex.get_animation(9, "background")
self._id_overlay = tex.get_texture(self.path .. "/" .. self.name .. "/overlay") or 0
return self
end
@@ -51,7 +51,7 @@ function BGNormal2.new(index, path)
local self = setmetatable({}, BGNormal2)
local base = BGNormalBase.new(index, path)
for k, v in pairs(base) do self[k] = v end
self.flicker = tex.get_animation(10)
self.flicker = tex.get_animation(10, "background")
self._id_center = tex.get_texture(self.path .. "/" .. self.name .. "/center") or 0
self._id_overlay = tex.get_texture(self.path .. "/" .. self.name .. "/overlay") or 0
self._id_lamps = tex.get_texture(self.path .. "/" .. self.name .. "/lamps") or 0
@@ -115,9 +115,9 @@ function BGNormal3.new(index, path)
local self = setmetatable({}, BGNormal3)
local base = BGNormalBase.new(index, path)
for k, v in pairs(base) do self[k] = v end
self.flicker = tex.get_animation(11)
self.turtle_move = tex.get_animation(12)
self.turtle_change = tex.get_animation(13)
self.flicker = tex.get_animation(11, "background")
self.turtle_move = tex.get_animation(12, "background")
self.turtle_change = tex.get_animation(13, "background")
self.screen_scale = tex.get_screen_scale()
self.screen_width = tex.get_screen_width()
self.screen_height = tex.get_screen_height()
+1 -1
View File
@@ -5,7 +5,7 @@ local DonBG4 = require("background/bg_objects/donbg/4")
local DonBG5 = require("background/bg_objects/donbg/5")
local DonBG6 = require("background/bg_objects/donbg/6")
DonBG = {}
local DonBG = {}
function DonBG.create(player_num, path, index)
index = index or math.random(1, 6)
+2 -2
View File
@@ -3,14 +3,14 @@
-- base contract.
local DonBGBase = require("background/bg_objects/donbg/base")
DonBG4 = setmetatable({}, { __index = DonBGBase })
local DonBG4 = setmetatable({}, { __index = DonBGBase })
DonBG4.__index = DonBG4
function DonBG4.new(index, player_num, path)
local self = setmetatable({}, DonBG4)
local base = DonBGBase.new(index, player_num, path)
for k, v in pairs(base) do self[k] = v end
self.overlay_move = tex.get_animation(2)
self.overlay_move = tex.get_animation(2, "background")
self.overlay_texture = tex.get_texture_info(self.name, "overlay")
self._id_overlay = tex.get_texture(self.path .. "/" .. self.name .. "/overlay") -- nil = missing
return self
+7 -3
View File
@@ -2,7 +2,7 @@
-- adds the cabinet's three sakura-petal drifters (between the two scrolling layers).
local DonBGBase = require("background/bg_objects/donbg/base")
DonBG6 = setmetatable({}, { __index = DonBGBase })
local DonBG6 = setmetatable({}, { __index = DonBGBase })
DonBG6.__index = DonBG6
local PETAL_W, PETAL_H = 391, 208
@@ -56,9 +56,9 @@ end
-- Same shape as DonBGBase:draw(), with the drifters slotted in at their real
-- display-list depth: after the depth-0 backdrop, before the depth-4 motif row.
function DonBG6:draw(y)
function DonBG6:draw_band(y)
y = y or 0
if not self.use_layers then return DonBGBase.draw(self, y) end
if not self.use_layers then return DonBGBase.draw_band(self, y) end
local normal = self.states.normal or {}
local fever = self.states.fever
@@ -87,4 +87,8 @@ function DonBG6:draw(y)
end
end
function DonBG6:draw(y)
self:draw_dark(DonBG6.draw_band, y or 0)
end
return DonBG6
+45 -3
View File
@@ -8,7 +8,7 @@ if not ok or type(DATA) ~= "table" then DATA = {} end
local ok_anim, DonBGAnim = pcall(require, "background/bg_objects/donbg/anim_player")
if not ok_anim or type(DonBGAnim) ~= "table" then DonBGAnim = nil end
DonBGBase = {}
local DonBGBase = {}
DonBGBase.__index = DonBGBase
function DonBGBase.new(index, player_num, path)
@@ -33,7 +33,7 @@ function DonBGBase.new(index, player_num, path)
end
self.is_clear = false
self.clear_fade = tex.get_animation(1)
self.clear_fade = tex.get_animation(1, "background")
-- exit is a reverse fade of the same length the bg_fever wipe uses
self.exit_fade = nil
@@ -77,6 +77,27 @@ end
local EXIT_MS = 29000 / 60
-- miss streak: the band is multiplied towards black (colour of draw_texture), eased in/out
local DARK_MIN, DARK_FADE_MS = 0.65, 300
function DonBGBase:set_dark(on)
self.dark_target = on and 1 or 0
end
function DonBGBase:update_dark(current_time_ms)
local dt = current_time_ms - (self.dark_last_ms or current_time_ms)
self.dark_last_ms = current_time_ms
local d, target = self.dark or 0, self.dark_target or 0
local step = dt / DARK_FADE_MS
self.dark = d + math.max(-step, math.min(step, target - d))
end
function DonBGBase:dark_color()
if not self.dark or self.dark <= 0 then return nil end
local v = math.floor(255 * (1 - (1 - DARK_MIN) * self.dark))
return { v, v, v, 255 }
end
function DonBGBase:set_clear(is_clear)
if is_clear and not self.is_clear then
self.is_clear = true
@@ -98,6 +119,7 @@ function DonBGBase:set_clear(is_clear)
end
function DonBGBase:update(current_time_ms)
self:update_dark(current_time_ms)
-- same wall-clock accumulator bg_fever uses; guard drops the load-time gap
if self.last_ms then
local dt = current_time_ms - self.last_ms
@@ -159,7 +181,7 @@ function DonBGBase:draw_textures(fade, y, frame)
end
end
function DonBGBase:draw(y)
function DonBGBase:draw_band(y)
y = y or 0
-- collab path = parent contract; default pack = DATA-driven layer lists
@@ -191,6 +213,26 @@ function DonBGBase:draw(y)
end
end
-- Draws the band through `inner`, multiplying every texture it draws by the miss-streak
-- darkness. Subclasses draw with their own tex.draw_texture calls, so wrap that once here.
function DonBGBase:draw_dark(inner, y)
local c = self:dark_color()
if not c then return inner(self, y) end
local orig = tex.draw_texture
tex.draw_texture = function(id, p)
p = p or {}
p.color = c
return orig(id, p)
end
local ok, err = pcall(inner, self, y)
tex.draw_texture = orig
if not ok then error(err, 0) end
end
function DonBGBase:draw(y)
self:draw_dark(DonBGBase.draw_band, y or 0)
end
function DonBGBase:destroy()
tex.unload_folder(self.path, string.format("donbg/%s", self.name))
if self.anim then tex.unload_folder(self.path, self.anim.subset) end
+29 -8
View File
@@ -1,7 +1,25 @@
-- DAN_SELECT paint surface: selection cursor breathing loop and arrow chips,
-- sampled from the cabinet's own clips.
DanSelect = {}
DanSelect.__index = DanSelect
-- sampled from the cabinet's own clips. The dan boxes themselves are drawn by the
-- parent skin's dan_select.lua, which is loaded first and extended here.
-- the parent skin's copy of this script: the second match on package.path (the first is this file)
local function load_parent(name)
local seen = false
for tmpl in package.path:gmatch("[^;]+") do
local chunk = loadfile((tmpl:gsub("%?", name)))
if chunk then
if seen then return chunk end
seen = true
end
end
end
local parent = load_parent("dan_select/dan_select")
if parent then parent() end
if not DanSelect then
DanSelect = {}
DanSelect.__index = DanSelect
end
local parent_new = DanSelect.new
local ok, anim = pcall(require, "anim/sampler")
if not ok then anim = { safe = function()
@@ -16,11 +34,12 @@ local CUR = anim.safe("anim/dan_cursor")
local ARW = anim.safe("anim/dan_arrow")
local ARW_OFF = ARW:label("arrow_off") or 61
-- No real clip data at all -> no draw_cursor: the engine draws the cursor from its
-- inline tables (the boxes are still drawn by the parent's DanSelect).
local HAS_CLIPS = CUR:has("#259@0") or ARW:has("#261@0")
function DanSelect.new()
-- No real clip data at all -> hand the whole draw back to the engine's
-- inline tables (returning nil leaves fn_draw_cursor invalid, C++ side).
if not CUR:has("#259@0") and not ARW:has("#261@0") then return nil end
local self = setmetatable({}, DanSelect)
local self = parent_new and parent_new() or setmetatable({}, DanSelect)
self.t_cursor = tex.get_texture("dan_select/box/cursor")
self.t_arrow_r = tex.get_texture("dan_select/box/arrow_r")
self.t_arrow_l = tex.get_texture("dan_select/box/arrow_l")
@@ -29,7 +48,7 @@ end
-- pos: selected chip x (the textures' own json positions carry the rest);
-- now_ms: engine clock; moved_ms: ms since the last ribbon step (-1 = never).
function DanSelect:draw_cursor(pos, now_ms, moved_ms)
local function draw_cursor(self, pos, now_ms, moved_ms)
if self.t_cursor then
local f = (now_ms * 0.06) % 60
local a = CUR:get("#259@0", f, "a") or 1
@@ -51,3 +70,5 @@ function DanSelect:draw_cursor(pos, now_ms, moved_ms)
end
end
end
if HAS_CLIPS then DanSelect.draw_cursor = draw_cursor end
-3
View File
@@ -4,7 +4,6 @@ Entry.__index = Entry
function Entry.new()
local self = setmetatable({}, Entry)
__hss_entry_reset = true
tex.load_animations("entry")
self.bg_flicker = tex.get_animation(1, "entry")
self.side_select_fade = tex.get_animation(0, "entry")
@@ -45,7 +44,6 @@ function Entry.new()
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.get_texture("entry/global/player_entry")
self.guide = tex.get_texture("global/indicator/background")
self.guide_t0 = self.cr_t0
@@ -238,7 +236,6 @@ function Entry:draw_footer(p1_joined, p2_joined)
end
function Entry:draw_player_entry()
tex.draw_texture(self.global_player_entry)
self:draw_credit_decide()
end
+9 -3
View File
@@ -35,13 +35,13 @@ function EntryPlayer.new(side)
return self
end
function EntryPlayer:start_animations()
-- hold_cloud: login still loading; the cloud keeps puffing/looping until finish_cloud()
function EntryPlayer:start_animations(hold_cloud)
self.drum_move_1:start()
self.drum_move_3:start()
self.cloud_resize:start()
self.cloud_resize_loop:start()
self.cloud_texture_change:start()
self.cloud_fade:start()
if not hold_cloud then self:finish_cloud() end
self.nameplate_fadein:start()
end
@@ -177,3 +177,9 @@ end
function EntryPlayer:get_nameplate_fade()
return self.nameplate_fadein.attribute
end
-- let the cloud shrink away: texture change + fade out
function EntryPlayer:finish_cloud()
self.cloud_texture_change:start()
self.cloud_fade:start()
end
+10 -4
View File
@@ -7,11 +7,13 @@ function AllNetIcon.new(online)
local self = setmetatable({}, AllNetIcon)
self.online = online
self.indicator = tex.get_texture("global/overlay/allnet_indicator")
self.params = { frame = online and 2 or 0, x = 0, y = 0 } -- reused every draw, no per-frame table garbage
return self
end
function AllNetIcon:update(current_ms, online)
self.online = online
self.params.frame = online and 2 or 0
end
local SONG_MAX_CX, SONG_MAX_CY = 1814, 122
@@ -36,20 +38,24 @@ function AllNetIcon:draw_song_max()
y = math.floor(SONG_MAX_CY - t.height / 2 + 0.5), fade = fade })
end
function AllNetIcon:draw_icon(x, y)
local p = self.params
p.x, p.y = x, y
tex.draw_texture(self.indicator, p)
end
function AllNetIcon:draw(x, y)
local screen = tex.current_screen and tex.current_screen() or ""
if screen == "GAME" or screen == "GAME_DAN" then
if screen == "GAME" and not (tex.in_transition and tex.in_transition()) then
self:draw_song_max()
end
local frame = self.online and 2 or 0
tex.draw_texture(self.indicator, { frame = frame, x = x, y = y })
self:draw_icon(x, y)
if _G.g_active_enso_skip then _G.g_active_enso_skip:draw() end
return
end
if screen == "LOADING" then
return
end
local frame = self.online and 2 or 0
tex.draw_texture(self.indicator, { frame = frame, x = x, y = y })
self:draw_icon(x, y)
end
+17 -1
View File
@@ -91,9 +91,25 @@ function SongTransition:skip_armed()
return self.skip_on
end
-- timeline the engine reads (rainbow rise for the title, title fades, done)
local RISE_MS, RISE_DIST = 266.0, 1224 -- 816 at 720p, scaled to 1080p
function SongTransition:state()
local t = self.t or 0
local rise = RISE_DIST * clamp(t / RISE_MS, 0, 1) ^ 3
if self.is_second then
return { rainbow_up = rise, fade_out = clamp(1.0 - t / FADE_OUT_MS, 0, 1),
finished = t >= FADE_OUT_MS }
end
return { rainbow_up = rise, fade = clamp((t - FADE_DELAY) / FADE_MS, 0, 1),
finished = t >= SCENE_MS }
end
function SongTransition:start() self.t0 = nil end
function SongTransition:update(current_ms, rainbow_up, song_info_fade)
if not self.t0 then self.t0 = current_ms end
local t = clamp(current_ms - self.t0, 0, SCENE_MS)
self.t = current_ms - self.t0
local t = clamp(self.t, 0, SCENE_MS)
if self.is_second then
self.f = F_OUT + (F_END - F_OUT) * (t / SCENE_MS)
self.info = clamp(1.0 - t / FADE_OUT_MS, 0, 1)
-1
View File
@@ -3,7 +3,6 @@ BottomCharacters.__index = BottomCharacters
function BottomCharacters.new()
local self = setmetatable({}, BottomCharacters)
tex.load_animations("result")
self.move_up = tex.get_animation(7, "result")
self.move_down = tex.get_animation(8, "result")
-1
View File
@@ -12,7 +12,6 @@ local BAR_BASE_Y = 266
function HighScoreIndicator.new(score_diff, is_2p)
local self = setmetatable({}, HighScoreIndicator)
tex.load_animations("result")
self.start_ms = nil
self.y_offset = 24
self.fade_val = 0
-1
View File
@@ -8,7 +8,6 @@ if not ok_layers then Layers = nil end
function ResultFadeIn.new(player_num)
local self = setmetatable({}, ResultFadeIn)
self.player_num = player_num
tex.load_animations("result")
__hss_result_bg_clear = 0.0 -- first object the result scene builds; clear last run's state
self.fade_in = tex.get_animation(15, "result")
self.fade_in:start()
+12 -2
View File
@@ -99,9 +99,12 @@ function ResultPlayer.new(player_num, is_2p, has_2p,
crown_type, score_diff, score_method,
mod_display, mod_inverse, mod_random, mod_speed)
local self = setmetatable({}, ResultPlayer)
tex.load_animations("result")
self.player_num = player_num
local timing = tex.timing_offset(player_num)
if timing then
self.timing_text = text.create_raw_text(timing, 20 * tex.get_screen_scale(), {255, 255, 255, 255}, {0, 0, 0, 255}, false, 3.0)
end
self.is_2p = is_2p
self.has_2p = has_2p
self.gauge_length = gauge_length
@@ -327,7 +330,7 @@ function ResultPlayer:update(current_ms, fade_in_finished,
self.rank_obj = ClipPlayer.new("game/ending_anim/" .. clip, "game", clip .. "_anim", pos[1], pos[2])
if self.rank_obj then
self.rank_obj.hold_last = true
if rank < 7 then self.rank_obj.shape_map = { [RANK_SHAPES[1]] = RANK_SHAPES[rank] } end
if rank < 7 then self.rank_obj.shape_map = { [74] = RANK_SHAPES[rank], [76] = RANK_SHAPES[rank] } end -- clip uses plate 74 then 76
self.rank_obj:start(current_ms)
-- se_result `scorerank_NN_<seat>`: one sample for every rank, seat like the crown
audio.play_sound("scorerank" .. (self.has_2p and (self.is_2p and "_r" or "_l") or "_c"), "sound")
@@ -389,6 +392,13 @@ function ResultPlayer:draw()
-- board frame (opaque white body), then the red/blue donback panel on top of it, then the difficulty tab
if self.t_board then tex.draw_texture(self.t_board, { frame = p_idx, index = p_idx }) end
if self.t_donback then tex.draw_texture(self.t_donback, { frame = p_idx, index = p_idx }) end
if self.timing_text then
local info = tex.get_texture_info("bottom", "chara_0")
if info and info.x[p_idx + 1] then
local pad = 6 * tex.get_screen_scale()
self.timing_text:draw({ x = info.x[p_idx + 1] + pad, y = info.y[p_idx + 1] + pad })
end
end
if self.t_difficulty then tex.draw_texture(self.t_difficulty, { frame = self.selected_difficulty, index = p_idx }) end
-- option icons
File renamed without changes.
+199
View File
@@ -0,0 +1,199 @@
-- Difficulty sort window, split out of song_select.lua.
local base = require("song_select/file_navigator/box_base")
local draw_centered = base.draw_centered
local draw_title = base.draw_title
local draw_guide_label = base.draw_guide_label
local outline = base.outline
local GUIDE_SELECT_CX, GUIDE_DECIDE_CX = base.GUIDE_SELECT_CX, base.GUIDE_DECIDE_CX
local SW_ROW_Y = { 410, 551, 692 }
local SW_BOARD_W, SW_BOARD_H = 1320, 760
local SW_CURSOR_W, SW_CURSOR_H = 1240, 180
local SW_PILL_CX = 1124 -- 960 + 164
local SW_PILL_W, SW_PILL_H = 792, 104
local SW_MARK_CX = 871 -- 960 - 89
local SW_ARROW_LX = 786 -- 960 - 174
local SW_ARROW_RX = 1462 -- 960 + 502
local SW_HEADER_CX = 560 -- 960 - 400
local SW_TEXT_DY = -13
local SW_GUIDE_Y = 827
local SW_GUIDE_W, SW_GUIDE_H = 372, 96
local SW_NUM_RIGHT = 1379
local SW_NUM_Y = 289
local SW_NUM_PITCH = 34
local SW_NUM_W, SW_NUM_H = 48, 56
local SW_SONGS_X = 1402 -- ink LEFT edge (EditText align = left)
local SW_SONGS_Y = 277
local SW_TITLE_X = 405
local SW_TITLE_Y = 272
local SW_TITLE_OUTLINE = { 119, 26, 45, 255 }
local SW_HEADER_BOX_W = 310
local SW_ITEM_BOX_W = { 620, 430, 430 }
local SW_CURSOR_CYCLE = 51.0
local SW_KEYS = {
header = { "sort_window_diff", "sort_window_star", "sort_window_order" },
course = { "course_name_0", "course_name_1", "course_name_2", "course_name_3", "course_name_3" },
order = { "sort_window_order_0", "sort_window_order_1", "sort_window_order_2", "sort_window_order_3" },
title = "folder_sort_select",
songs = "sort_window_songs",
}
local function sw_words(group)
local k = SW_KEYS[group]
if type(k) == "table" then
local out = {}
for i, key in ipairs(k) do out[i] = tex.skin_text(key) end
return out
end
return tex.skin_text(k)
end
-- OutlinedText cache: key = group .. "\0" .. index.
function SongSelect:_sw_text(key, str, size, thickness, outline)
self._sw_texts = self._sw_texts or {}
local t = self._sw_texts[key]
if t == nil then
t = text.create_raw_text(str, size, { 255, 255, 255, 255 },
outline or { 0, 0, 0, 255 }, false, thickness, 2.0)
self._sw_texts[key] = t or false
end
return t or nil
end
-- Fit-shrink: when a string does not fit its box, re-rasterise it at a smaller (uniformly
-- scaled) point size instead of stretching the bitmap, so glyphs keep their proportions.
local SW_FIT_MIN_SIZE = 22
function SongSelect:_sw_text_fit(key, str, size, thickness, outline, max_w)
self._sw_texts = self._sw_texts or {}
local t = self._sw_texts[key]
if t == nil then
t = text.create_raw_text(str, size, { 255, 255, 255, 255 },
outline or { 0, 0, 0, 255 }, false, thickness, 2.0)
if t and max_w and t.width > max_w then
local fit_size = math.max(SW_FIT_MIN_SIZE, math.floor(size * max_w / t.width))
if fit_size < size then
local fit_thickness = thickness * fit_size / size
local t2 = text.create_raw_text(str, fit_size, { 255, 255, 255, 255 },
outline or { 0, 0, 0, 255 }, false, fit_thickness, 2.0)
if t2 then t = t2 end
end
end
self._sw_texts[key] = t or false
end
return t or nil
end
-- Draw an OutlinedText with its ink LEFT edge on x.
local function draw_text_left(t, x, cy, fade)
if not t then return end
t:draw({ x = math.floor(x + 0.5), y = math.floor(cy - t.height / 2 + 0.5), fade = fade })
end
-- cursor glow alpha over the 51-frame loop, sampled from anim/sort_cursor.
local sort_cursor_ok, sort_cursor_anim = pcall(require, "anim/sampler")
local SW_CURSOR_CLIP = (sort_cursor_ok and sort_cursor_anim.safe("anim/sort_cursor"))
or { get = function() return nil end, label = function() return nil end }
local SW_ON_F = SW_CURSOR_CLIP:label("on") or 9
local function sw_cursor_glow(ms)
local r = (ms * 0.06) % SW_CURSOR_CYCLE
local a = SW_CURSOR_CLIP:get("#3@0", SW_ON_F + r, "a")
if a then return a end
if r <= 20.0 then return 1.0 - r / 20.0 end
return (r - 20.0) / 30.0
end
function SongSelect:_sw_ids()
if self._sw_loaded then return end
tex.load_folder("song_select", "diff_sort")
self._id_sw_board = tex.get_texture("song_select/diff_sort/background") -- 1320x760 window
self._id_sw_pill = tex.get_texture("song_select/diff_sort/level_box") -- 792x104 white item pill
self._id_sw_cur = tex.get_texture("song_select/diff_sort/box_highlight") -- 1240x180 flat yellow bar
self._id_sw_glow = tex.get_texture("song_select/diff_sort/box_outline") -- 1240x180 pulsing glow
self._id_sw_mark = tex.get_texture("song_select/diff_sort/diff") -- 96x96, frame = course
self._id_sw_arrow = tex.get_texture("song_select/diff_sort/arrow") -- 48x56, frame 1 = rotated
self._id_sw_digit = tex.get_texture("song_select/diff_sort/stat_num") -- 48x56, frame = digit
self._id_sw_guide = tex.get_texture("song_select/diff_sort/star_select_text") -- s_menfuchi_kp loop
self._sw_loaded = true
end
function SongSelect:draw_sort_window(w)
self:_sw_ids()
local a = w:alpha()
if a <= 0.0 or not self._id_sw_board then return end
local ms = tex.get_current_ms()
local session = w:session()
local vals = { w:diff(), w:star(), w:order() }
draw_centered(self._id_sw_board, 960, 540, SW_BOARD_W, SW_BOARD_H, { fade = a })
-- the three rows, in the cabinet's own depth order (order, star, diff)
for row = 3, 1, -1 do
local ry = SW_ROW_Y[row]
local idx = row - 1 -- 0-based, matching SortWindow:session()
if idx == session and self._id_sw_cur then
if self._id_sw_glow then
draw_centered(self._id_sw_glow, 960, ry, SW_CURSOR_W, SW_CURSOR_H,
{ fade = a * sw_cursor_glow(ms) })
end
draw_centered(self._id_sw_cur, 960, ry, SW_CURSOR_W, SW_CURSOR_H, { fade = a })
end
if self._id_sw_pill then
draw_centered(self._id_sw_pill, SW_PILL_CX, ry, SW_PILL_W, SW_PILL_H, { fade = a })
end
-- diff_mark is visible on the むずかしさ row only
if row == 1 and self._id_sw_mark then
draw_centered(self._id_sw_mark, SW_MARK_CX, ry, 96, 96,
{ frame = math.max(0, math.min(4, vals[1] - 1)), fade = a })
end
if self._id_sw_arrow then
local dx = w:arrow_offset(idx)
draw_centered(self._id_sw_arrow, SW_ARROW_LX + dx, ry, 48, 56, { frame = 0, fade = a })
draw_centered(self._id_sw_arrow, SW_ARROW_RX - dx, ry, 48, 56, { frame = 1, fade = a })
end
-- item value
local str, key
if row == 1 then
local n = math.max(1, math.min(5, vals[1]))
str, key = sw_words("course")[n], "course" .. n
elseif row == 2 then
str, key = tostring(vals[2]), "star" .. vals[2]
else
local n = math.max(1, math.min(4, vals[3]))
str, key = sw_words("order")[n], "order" .. n
end
draw_title(self:_sw_text_fit(key, str, 42, outline(6.5), nil, SW_ITEM_BOX_W[row]),
SW_PILL_CX, ry + SW_TEXT_DY, a, SW_ITEM_BOX_W[row], 0)
-- header
draw_title(self:_sw_text_fit("hdr" .. row, sw_words("header")[row], 42, outline(6.5), nil, SW_HEADER_BOX_W),
SW_HEADER_CX, ry + SW_TEXT_DY, a, SW_HEADER_BOX_W, 0)
end
-- control guide + its two pill labels
if self._id_sw_guide then
local cell = math.floor((ms * 0.06) % 210)
draw_centered(self._id_sw_guide, 960, SW_GUIDE_Y, SW_GUIDE_W, SW_GUIDE_H,
{ frame = cell, fade = a })
local labels = self:guide_labels()
if labels then
draw_guide_label(labels.select, GUIDE_SELECT_CX, SW_GUIDE_Y + 2, a)
draw_guide_label(labels.decide, GUIDE_DECIDE_CX, SW_GUIDE_Y + 2, a)
end
end
-- song counter: 「N 曲」 (right-aligned digits, then the unit label)
local n = math.max(0, math.min(9999, w:song_num()))
local digits = tostring(n)
for i = 1, #digits do
local d = tonumber(digits:sub(i, i))
local x = SW_NUM_RIGHT - (#digits - i) * SW_NUM_PITCH
draw_centered(self._id_sw_digit, x, SW_NUM_Y, SW_NUM_W, SW_NUM_H, { frame = d, fade = a })
end
draw_text_left(self:_sw_text("songs", sw_words("songs"), 42, outline(6.5)), SW_SONGS_X, SW_SONGS_Y, a)
-- board title, with the cabinet's dark-red outline add
draw_text_left(self:_sw_text("title", sw_words("title"), 54, outline(7.5), SW_TITLE_OUTLINE),
SW_TITLE_X, SW_TITLE_Y, a)
end
@@ -0,0 +1,77 @@
-- BackBox rendering ("back" brown board with a label), split out of song_select.lua.
local base = require("song_select/file_navigator/box_base")
local folder_leg = base.folder_leg
local BOARD_W = base.BOARD_W
local CEN_CLOSED_H, CEN_OPEN_H = base.CEN_CLOSED_H, base.CEN_OPEN_H
local FOLDER_CLOSE_MS, STALE_TIMER_MS = base.FOLDER_CLOSE_MS, base.STALE_TIMER_MS
local BOARD_OUTLINE_BACK = base.BOARD_OUTLINE_BACK
local draw_board, draw_glow, glow_pulse = base.draw_board, base.draw_glow, base.glow_pulse
local draw_title = base.draw_title
local outline = base.outline
local box_id = base.box_id
local go_get, leg_f, box_fade, fly_offset = base.go_get, base.leg_f, base.box_fade, base.fly_offset
local wheel_open_t, wheel_close_step = base.wheel_open_t, base.wheel_close_step
local folder_open_timeline, folder_close_timeline = base.folder_open_timeline, base.folder_close_timeline
function SongSelect:back_label()
if not self._back_label then
local cfg = tex.get_skin_config("song_back_label")
if cfg then
self._back_label = text.create_text("song_back_label", { 255, 255, 255, 255 }, BOARD_OUTLINE_BACK, false, outline(5.0), 2.0)
else
self._back_label = text.create_raw_text("BACK", 44, { 255, 255, 255, 255 }, BOARD_OUTLINE_BACK, false, outline(5.0), 2.0)
end
end
return self._back_label
end
local function back_pb_open(box)
local key = "b\0" .. box_id(box)
local t = wheel_open_t(key)
if t == nil then return 0.0 end
return (folder_open_timeline(t))
end
local function back_pb_closed(box)
local key = "b\0" .. box_id(box)
local e = wheel_close_step(key, FOLDER_CLOSE_MS, STALE_TIMER_MS)
if e == nil then return 0.0 end
return (folder_close_timeline(e))
end
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 -- 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 lta = 1.0
local lf = (box:draw_state() == "open") and leg_f() or nil
if lf and folder_leg.kind == "open" and lf < folder_leg.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
if ba <= 0.0 then return end
if sx < 1.0 then
draw_board(self._id_bar_back, 0, bx, by, CEN_OPEN_H,
fade * ba, false, sx)
return
end
lta = go_get("song_title_a", lf, 1) or 1.0
end
end
if p > 0 then draw_glow(self._id_glow, bx, by, cen_h, fade * p * glow_pulse()) end
draw_board(self._id_bar_back, 0, bx, by, cen_h, fade)
draw_title(self:back_label(), bx, by, fade * lta)
end
function SongSelect:draw_back_closed(box)
self:draw_back_board(box, back_pb_closed(box))
end
function SongSelect:draw_back_open(box)
self:draw_back_board(box, back_pb_open(box))
end
@@ -0,0 +1,573 @@
-- Shared song-select board chrome, text/colour helpers and the wheel/folder/song
-- animation timers, split out of song_select.lua. All positions are 1080p pixels;
-- box positions and engine animations are already in screen pixels.
local DIFF_BACK, DIFF_MODIFIER, DIFF_NEIRO = -3, -2, -1
local DIFF_EASY, DIFF_NORMAL, DIFF_HARD, DIFF_ONI, DIFF_URA = 0, 1, 2, 3, 4
local CROWN_NONE, CROWN_CLEAR, CROWN_FC, CROWN_DFC = 0, 1, 2, 3
-- Arcade board geometry (960x352 board texture: top/bottom caps + centre slice).
local BOARD_W = 960
local CAP_H = 56
local CEN_SRC_H = 240
local CEN_CLOSED_H = 52
local CEN_OPEN_H = 240
local GLOW_CAP_H = 80 -- cursor glow 1024x400: 80 / 240 / 80
local GLOW_W = 1024
local GUIDE_SELECT_CX, GUIDE_DECIDE_CX, GUIDE_LABEL_CY = 904, 1077, 980
local GUIDE_LABEL_SPACING = 2.0
-- tex.get_texture tries "<name>" then "<name>_<language>"; fall back to _en/_ja so we
-- never hand draw_texture a nil id.
local function get_any(subset, name)
return tex.get_texture("song_select/" .. subset .. "/" .. name)
or tex.get_texture("song_select/" .. subset .. "/" .. name .. "_en")
or tex.get_texture("song_select/" .. subset .. "/" .. name .. "_ja")
end
-- draw a w x h texture centred on (cx, cy)
local function draw_centered(id, cx, cy, w, h, extra)
extra = extra or {}
extra.x = cx - w / 2
extra.y = cy - h / 2
tex.draw_texture(id, extra)
end
-- Same, but subtracts the texture.json base offset the engine adds when it draws in 2P.
local function draw_centered_b(id, base, cx, cy, w, h, extra)
extra = extra or {}
extra.x = cx - w / 2 - base[1]
extra.y = cy - h / 2 - base[2]
tex.draw_texture(id, extra)
end
local SEL_BASE_BALLOON = { 683, 308 } -- 1p/2p_balloon(_half) = cursor bubble
-- Title centred on (bx, cy). max_w (optional): squeeze long text horizontally down to the
-- box width, never below min_scale (default 0.75). Destination x/y snap to whole pixels.
local function draw_title(text, bx, cy, fade, max_w, min_scale)
if not text then return end
local w = text.width
if max_w and w > max_w then
w = math.floor(math.max(max_w, w * (min_scale or 0.75)) + 0.5)
end
text:draw({ x = math.floor(bx - w / 2 + 0.5), y = math.floor(cy - text.height / 2 + 0.5),
x2 = w - text.width, fade = fade })
end
-- control-guide pill labels
local function draw_guide_label(t, cx, cy, fade)
if not t then return end
t:draw({ x = math.floor(cx - (t.width - GUIDE_LABEL_SPACING) / 2 + 0.5),
y = math.floor(cy - t.height / 2 + 0.5), fade = fade })
end
local SONG_TEXT_BOX_W = 860 -- main_song_board text_title / text_sub box width
local DS_TEXT_BOX_W = 1000 -- main_diff text_title_song / text_song_title_sub box width
-- OutlinedText multiplies the outline thickness by screen_scale, so divide the stage-px
-- border back out.
local function outline(border) return border / tex.get_screen_scale() end
-- Board title/subtitle outline colour by genre frame (0..12).
local BOARD_OUTLINE = {
[0] = { 18, 72, 76, 255 },
[1] = { 18, 72, 76, 255 },
[2] = { 155, 24, 99, 255 },
[3] = { 78, 29, 118, 255 },
[4] = { 150, 31, 0, 255 },
[5] = { 101, 67, 42, 255 },
[6] = { 20, 79, 20, 255 },
[7] = { 165, 50, 0, 255 },
[8] = { 38, 52, 73, 255 },
[9] = { 18, 72, 76, 255 },
[10] = { 0, 25, 95, 255 },
[11] = { 100, 6, 22, 255 },
[12] = { 119, 26, 45, 255 },
}
local BOARD_OUTLINE_BACK = { 76, 50, 20, 255 }
local function board_outline(frame)
return BOARD_OUTLINE[frame or 0] or BOARD_OUTLINE[0]
end
local function clamp01(v) return math.max(0.0, math.min(1.0, v)) end
-- Board chrome: 3-slice (cap / centre / cap) from a 960x352 texture, centred on (bx,by).
-- sx = optional horizontal scale (the folder-ENTER furl/unfurl); a furled board is drawn
-- from flat column slices so its edges stay sharp.
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 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
if crop_center ~= false and cen_h < CEN_SRC_H then
src_h = math.max(1, math.floor(cen_h + 0.5))
src_y = CAP_H + math.floor((CEN_SRC_H - src_h) / 2)
cen_h = src_h
end
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
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: draw the outline columns + a flat centre slice
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
end
-- Yellow cursor glow around the board.
local function draw_glow(glow_id, bx, by, cen_h, fade)
local x = math.floor(bx - GLOW_W / 2 + 0.5)
local top = math.floor(by - cen_h / 2 + 0.5) - CAP_H - 24
local src_h = math.max(1, math.min(240, math.floor(cen_h + 0.5)))
cen_h = src_h
tex.draw_texture(glow_id, { x = x, y = top, y2 = GLOW_CAP_H - 400, fade = fade,
src = { x = 0, y = 0, width = GLOW_W, height = GLOW_CAP_H } })
tex.draw_texture(glow_id, { x = x, y = top + GLOW_CAP_H, y2 = cen_h - 400, fade = fade,
src = { x = 0, y = GLOW_CAP_H + math.floor((240 - src_h) / 2), width = GLOW_W, height = src_h } })
tex.draw_texture(glow_id, { x = x, y = top + GLOW_CAP_H + cen_h, y2 = GLOW_CAP_H - 400, fade = fade,
src = { x = 0, y = GLOW_CAP_H + 240, width = GLOW_W, height = GLOW_CAP_H } })
end
local glow_ok, glow_anim = pcall(require, "anim/sampler")
if not glow_ok then glow_anim = { safe = function()
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
n.first = function() return 0 end; n.last = function() return 0 end
return n
end } end
local GLOW_CLIP = glow_anim.safe("anim/cursor_glow")
local GLOW_LEN = math.max(1, GLOW_CLIP:last() - GLOW_CLIP:first() + 1) -- 60 frames = 1 s
-- song-board oni/ura chip cross-fade loop
local URA_LOOP = glow_anim.safe("anim/song_board_ura")
local URA_LOOP_F0 = URA_LOOP:first() -- label 'ura' = f10
local URA_LOOP_LEN = math.max(1, URA_LOOP:last() - URA_LOOP_F0 + 1) -- 180 frames = 3 s
local function glow_pulse()
local f = (tex.get_current_ms() * 0.06) % GLOW_LEN
return GLOW_CLIP:get("#12@0", f, "a") or 1.0
end
-- Arcade genre board open / close, sampled from anim/folder_board.
local folder_anim = glow_anim
local FOLDER_CLIP = folder_anim.safe("anim/folder_board")
local F_SELECT_ON = FOLDER_CLIP:label("select_on") or 5
local F_SELECT_OFF = FOLDER_CLIP:label("select_off") or 30
local FOLDER_CLOSE_END_F = 8 -- select_off + 8 = f38, the last frame that moves
local FOLDER_CLOSE_MS = FOLDER_CLOSE_END_F / 0.06
-- Duration the engine keeps the box "closed" between navigate() and expand_box().
local NAV_REOPEN_MS = 166
-- A transient navigation closure is < NAV_REOPEN_MS; anything longer resets the open timer.
local STALE_TIMER_MS = math.max(FOLDER_CLOSE_MS, NAV_REOPEN_MS) + 50
local CABINET_OPEN_WAIT_MS = 61 / 120 * 1000 -- = 508.33 ms
local EXTRA_OPEN_HOLD_MS = math.max(0, CABINET_OPEN_WAIT_MS - NAV_REOPEN_MS) -- = 342.33 ms
-- centre band scaleY at the two rest states -> 0..1 openness
local SY_CLOSED, SY_OPEN = 0.2168, 1.0
-- Song board timeline, same labels as the genre board.
local SONG_CLIP = folder_anim.safe("anim/song_board")
local S_SELECT_ON = SONG_CLIP:label("select_on") or 5
local S_SELECT_OFF = SONG_CLIP:label("select_off") or 30
local SONG_CLOSE_END_F = 13
local SONG_CLOSE_MS = SONG_CLOSE_END_F / 0.06 -- = 216.7 ms
local SONG_STALE_MS = math.max(SONG_CLOSE_MS, NAV_REOPEN_MS) + 50
-- Folder enter/exit wheel choreography: slots sweep out along the tilted line, hold, then
-- fly back in (sampled from anim/wheel_decide).
local WHEEL_FLY = folder_anim.safe("anim/wheel_decide")
local FLY_END_F = WHEEL_FLY:label("return") or 150
local FLY_TRACKS_UP = { "kanban_genre_2", "kanban_genre_4", "kanban_genre_6", "kanban_genre_8" }
local FLY_TRACKS_DOWN = { "kanban_genre_3", "kanban_genre_5", "kanban_genre_7" }
local FLY_CENTER_Y, FLY_ROW_GAP, FLY_ROW_PITCH = 540, 120, 135
do -- wheel geometry is skin_config-driven; follow it if restated
local ok, pitch = pcall(tex.get_skin_config, "song_select_row_pitch")
if ok and pitch and pitch.x and pitch.x > 0 then
FLY_ROW_PITCH = pitch.x
if pitch.y and pitch.y > 0 then FLY_ROW_GAP = pitch.y end
end
end
-- A wheel animation-timer key must be unique per BOX. text_name is not (two charts of one
-- song share a title), so prefer box:path() and fall back to title+subtitle+ex_data.
local function box_id(box)
local ok, p = pcall(function() return box:path() end)
if ok and p and p ~= "" then return p end
local id = box.text_name or ""
local ok_s, sub = pcall(function() return box.text_subtitle end)
if ok_s and sub then id = id .. "\1" .. sub end
local ok_x, x = pcall(function() return box:ex_data_flag() end)
if ok_x and x then id = id .. "\1" .. tostring(x) end
return id
end
-- Folder enter/exit leg state (anim/genre_open), driven by the engine's wheel events.
local folder_leg = {
clip = nil,
t0 = nil, f0 = 0, kind = nil, swapped = false,
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
seq = nil,
have_events = false,
snap_once = false, snap_ms = nil,
nohold_once = false, nohold_ms = nil,
}
do
local ok, t = pcall(require, "anim/genre_open")
if ok and type(t) == "table" and type(t.tracks) == "table" then
folder_leg.clip = t
for _, k in ipairs({ "open_f0", "open_swap", "open_end",
"close_f0", "close_swap", "close_end" }) do
if t[k] then folder_leg[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 folder_leg.open_furl = rows[i][1]; break end
end
end
end
end
-- Linear sampler over the generated {frame, v1, v2...} rows.
local function go_get(name, f, col)
if not folder_leg.clip then return nil end
local rows = folder_leg.clip.tracks[name]
if not rows or #rows == 0 then return nil end
col = col or 1
if f <= rows[1][1] then return rows[1][col + 1] end
if f >= rows[#rows][1] then return rows[#rows][col + 1] end
for i = 1, #rows - 1 do
local a, b = rows[i], rows[i + 1]
if f >= a[1] and f <= b[1] then
local span = b[1] - a[1]
if span <= 0 then return a[col + 1] end
local u = (f - a[1]) / span
return a[col + 1] + (b[col + 1] - a[col + 1]) * u
end
end
return rows[#rows][col + 1]
end
-- Current clip frame, clamped just short of the swap until the engine signals it.
local function leg_f()
if not folder_leg.clip or not folder_leg.t0 then return nil end
local f = folder_leg.f0 + (tex.get_current_ms() - folder_leg.t0) * 0.06
if not folder_leg.swapped then
-- hold until the swap: the open leg parks on the furl, the close leg one frame
-- short of its swap, so the board stays up as the loading state
local hold = (folder_leg.kind == "close") and (folder_leg.close_swap - 1) or (folder_leg.open_furl - 1)
if f > hold then f = hold end
return f
end
local fin = (folder_leg.kind == "close") and folder_leg.close_end or folder_leg.open_end
if f >= fin then folder_leg.t0 = nil; folder_leg.kind = nil; return nil end
return f
end
-- The engine's own box fade, ignored during the pre-swap half of the close leg.
local function box_fade(box)
if folder_leg.kind == "close" and folder_leg.t0 and not folder_leg.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 content-swap frame.
local function leg_event(id)
local now = tex.get_current_ms()
if id == 3 then -- OPEN_BEGIN
folder_leg.t0, folder_leg.f0, folder_leg.kind, folder_leg.swapped = now, folder_leg.open_f0, "open", false
folder_leg.dec_id, folder_leg.dec_box = folder_leg.last_open_id, folder_leg.last_open_box
elseif id == 4 then -- OPEN_SWAP
local f = (folder_leg.kind == "open" and folder_leg.t0) and (folder_leg.f0 + (now - folder_leg.t0) * 0.06) or nil
if not f or f >= folder_leg.open_furl - 1 then
folder_leg.t0, folder_leg.f0 = now, folder_leg.open_furl
end
folder_leg.kind, folder_leg.swapped = "open", true
folder_leg.snap_once, folder_leg.snap_ms = true, now
elseif id == 5 then -- CLOSE_BEGIN
folder_leg.t0, folder_leg.f0, folder_leg.kind, folder_leg.swapped = now, folder_leg.close_f0, "close", false
elseif id == 6 then -- CLOSE_SWAP
folder_leg.t0, folder_leg.f0, folder_leg.kind, folder_leg.swapped = now, folder_leg.close_swap, "close", true
folder_leg.snap_once, folder_leg.snap_ms = true, now
elseif id == 1 or id == 7 then -- SCENE_ENTRY / COURSE_BACK
folder_leg.nohold_once, folder_leg.nohold_ms = true, now
end
-- id == 2 (CURSOR_MOVE) needs no flag.
end
-- The wheel clip shares the board clips' labels, so one clock drives all three.
local function fly_offset(box)
local f = leg_f()
if not f then return 0.0, 0.0 end
if folder_leg.kind == "open" then
if f >= FLY_END_F then return 0.0, 0.0 end
elseif folder_leg.kind == "close" then
if f >= (WHEEL_FLY:label("genre_deceide2") or 255) - 1 then return 0.0, 0.0 end
else
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
local ring = math.floor((math.abs(d) - FLY_ROW_GAP) / FLY_ROW_PITCH + 0.5)
if ring < 1 then ring = 1 end
local track
if d < 0 then track = FLY_TRACKS_UP[math.min(ring, #FLY_TRACKS_UP)]
else track = FLY_TRACKS_DOWN[math.min(ring, #FLY_TRACKS_DOWN)] end
local x, y = WHEEL_FLY:get(track, f, "tx"), WHEEL_FLY:get(track, f, "ty")
if not x or not y then return 0.0, 0.0 end
local rx = WHEEL_FLY:get(track, FLY_END_F, "tx") or 0.0
local ry = WHEEL_FLY:get(track, FLY_END_F, "ty") or 0.0
return x - rx, y - ry
end
-- true while `box` is the genre board the player just decided on (open leg running)
local function is_decided(box)
return folder_leg.kind == "open" and folder_leg.t0 ~= nil and folder_leg.dec_id ~= nil and box_id(box) == folder_leg.dec_id
end
-- How a fresh open starts: "hold" (cursor move), "snap" (folder enter/exit), "nohold"
-- (scene entry / course back). One-shots are armed by the engine's real transitions.
-- (diff-seen timestamp lives on folder_leg so box_song can mutate it across the module split.)
local OPEN_SNAP_TTL_MS = 2000.0
local OPEN_NOHOLD_TTL_MS = 3000.0
local function open_start_mode(now)
if folder_leg.snap_once then
folder_leg.snap_once = false
if folder_leg.snap_ms and (now - folder_leg.snap_ms) < OPEN_SNAP_TTL_MS then return "snap" end
end
if folder_leg.nohold_once then
folder_leg.nohold_once = false
if folder_leg.nohold_ms and (now - folder_leg.nohold_ms) < OPEN_NOHOLD_TTL_MS then return "nohold" end
end
-- Legacy fallback for a binary without the event channel.
if not folder_leg.have_events and folder_leg.diff_seen_ms and (now - folder_leg.diff_seen_ms) < 300.0 then
return "nohold"
end
return "hold"
end
-- Shared wheel-panel open/close wall-clock timers. Keys are "<kind>\0<name>".
local wheel_open_start = {}
local wheel_open_mode = {}
local wheel_open_seen = {}
local wheel_close_at = {}
local wheel_close_start= {}
local wheel_last_draw = {}
local wheel_last_fresh_key, wheel_last_fresh_ms = nil, nil
-- Returns t in ms into the select_on growth window (0..233, clamped), or nil during the
-- 508 ms pre-growth hold (draw fully closed).
local function wheel_open_t(key)
local now = tex.get_current_ms()
wheel_close_start[key] = nil
wheel_open_seen[key] = now
wheel_close_at[key] = nil
-- off-screen for > NAV_REOPEN_MS with the animation finished -> reset the timer
local last = wheel_last_draw[key]
wheel_last_draw[key] = now
if wheel_open_start[key] and last and (now - last) > NAV_REOPEN_MS then
local hold = (wheel_open_mode[key] == "hold") and EXTRA_OPEN_HOLD_MS or 0.0
if (now - wheel_open_start[key]) >= (hold + 233.0) then
wheel_open_start[key] = nil
end
end
if not wheel_open_start[key] then
wheel_open_start[key] = now
wheel_open_mode[key] = open_start_mode(now)
wheel_last_fresh_key, wheel_last_fresh_ms = key, now
end
local mode = wheel_open_mode[key]
if mode == "snap" then return 233.0 end
local elapsed = now - wheel_open_start[key]
local hold = (mode == "hold") and EXTRA_OPEN_HOLD_MS or 0.0
if elapsed < hold then return nil end
return math.min(elapsed - hold, 233.0)
end
-- Closed-state bookkeeping + close animation. Returns e in ms into the select_off
-- timeline while closing, else nil (draw at rest).
local function wheel_close_step(key, close_ms, stale_ms)
local now = tex.get_current_ms()
if not wheel_close_start[key] then
wheel_close_start[key] = now
end
if wheel_open_start[key] and (now - wheel_close_start[key]) > stale_ms then
wheel_open_start[key] = nil
end
local seen = wheel_open_seen[key]
if seen and now - seen < 100 then -- was drawn open a frame ago -> start closing
wheel_close_at[key] = now
wheel_open_seen[key] = nil
end
local c = wheel_close_at[key]
if c then
local e = now - c
if e < close_ms then return e end
wheel_close_at[key] = nil
end
return nil
end
-- `f` is an absolute arcade frame on sprite 73's timeline.
-- pb = board progress 0..1, ch_alpha = character alpha, ch_off = character offset,
-- ta = intro ("N songs") text alpha.
local function folder_sample(f)
local sy = FOLDER_CLIP:get("instance_board_center", f, "sy") or SY_CLOSED
local pb = clamp01((sy - SY_CLOSED) / (SY_OPEN - SY_CLOSED))
local ch_alpha = FOLDER_CLIP:get("instance_character_l", f, "a") or 0.0
local ch_x = FOLDER_CLIP:get("instance_character_l", f, "tx") or -340.0
local ta = FOLDER_CLIP:get("text_kanban_intro_genre", f, "a") or 0.0
return pb, ch_alpha, -ch_x, ta
end
local function folder_open_timeline(t)
return folder_sample(F_SELECT_ON + t * 0.06)
end
local function folder_close_timeline(e)
return folder_sample(F_SELECT_OFF + e * 0.06)
end
-- Same reader for the song board: pb = board progress, ia = contents alpha, ic = NEW-icon alpha.
local function song_sample(f)
local sy = SONG_CLIP:get("instance_board_center", f, "sy") or SY_CLOSED
local pb = clamp01((sy - SY_CLOSED) / (SY_OPEN - SY_CLOSED))
local ia = SONG_CLIP:get("song_kanban_info", f, "a") or (pb >= 1.0 and 1.0 or 0.0)
local ic = SONG_CLIP:get("instance_icon", f, "a") or (1.0 - pb)
return pb, clamp01(ia), clamp01(ic)
end
local function song_open_timeline(t) return song_sample(S_SELECT_ON + t * 0.06) end
local function song_close_timeline(e) return song_sample(S_SELECT_OFF + e * 0.06) end
-- 演奏オプション header title of the C++ option panel.
local MOD_SLIDE, MOD_SLIDE_MS, MOD_JSON_Y, MOD_HEADER_CY, MOD_CX = 548, 333.33, 1080, 49, 215
-- Difficulty select geometry (shared by the diff-select board and the selector cursor).
local DS_SLOT_X = { [DIFF_BACK] = 440, [DIFF_MODIFIER] = 572, [DIFF_NEIRO] = 440,
[DIFF_EASY] = 745, [DIFF_NORMAL] = 960, [DIFF_HARD] = 1175, [DIFF_ONI] = 1390 }
local DS_BOARD_Y = 583
local DS_COURSE_NAME_DY = 49 -- course-name centre below the card origin
local URA_ANIM_MS = 1500 -- oni_main change_ura/change_oni: 90 frames @ 60 fps
local URA_SWAP_MS = 450 -- board_oni -> board_ura swap
local URA_CELLS = 90
local DS_BTN_Y = 462
local DS_CURSOR_Y = 370
local DS_NEIRO_Y = 602
-- main_diff_mark: 680x680, drawn under the buttons/cards.
local DS_MARK_X = { [1] = 200, [2] = 1720 }
local DS_MARK_Y = 408
-- main_diff_mark 'course_decide_start', one row per frame:
-- { mark alpha, mark y-scale, white alpha, white y-scale, white backing alpha }.
local DS_MARK_DECIDE = {
{ 0.5, 1, 0, 1, 0 },
{ 0.5938, 1.0447, 0.1914, 1.0447, 0.1016 },
{ 0.6797, 1.0847, 0.3594, 1.0847, 0.1992 },
{ 0.7539, 1.12, 0.5117, 1.12, 0.3008 },
{ 0.8203, 1.1506, 0.6406, 1.1506, 0.3984 },
{ 0.875, 1.1765, 0.75, 1.1765, 0.5 },
{ 0.9219, 1.1976, 0.8398, 1.1976, 0.6016 },
{ 0.9531, 1.2141, 0.9102, 1.2141, 0.6992 },
{ 0.9805, 1.2259, 0.9609, 1.2259, 0.8008 },
{ 0.9961, 1.2329, 0.9883, 1.2329, 0.8984 },
{ 1, 1.2353, 1, 1.2353, 1 },
{ 1, 1.2229, 0.9961, 1.2346, 1 },
{ 1, 1.2105, 0.9883, 1.2327, 1 },
{ 1, 1.1981, 0.9766, 1.2294, 1 },
{ 1, 1.1858, 0.957, 1.2248, 1 },
{ 1, 1.1733, 0.9297, 1.219, 1 },
{ 1, 1.161, 0.8984, 1.2118, 1 },
{ 1, 1.1486, 0.8633, 1.2033, 1 },
{ 1, 1.1362, 0.8242, 1.1936, 1 },
{ 1, 1.1238, 0.7773, 1.1825, 1 },
{ 1, 1.1114, 0.7227, 1.1701, 1 },
{ 1, 1.0991, 0.6641, 1.1564, 1 },
{ 1, 1.0867, 0.6016, 1.1414, 1 },
{ 1, 1.0743, 0.5312, 1.1251, 1 },
{ 1, 1.0619, 0.457, 1.1075, 1 },
{ 1, 1.0495, 0.375, 1.0886, 1 },
{ 1, 1.0371, 0.2891, 1.0684, 1 },
{ 1, 1.0247, 0.1992, 1.0469, 1 },
{ 1, 1.0124, 0.1016, 1.0241, 1 },
{ 1, 1, 0, 1, 1 },
}
local DS_MARK_DECIDE_MS = 1000 / 60
return {
DIFF_BACK = DIFF_BACK, DIFF_MODIFIER = DIFF_MODIFIER, DIFF_NEIRO = DIFF_NEIRO,
DIFF_EASY = DIFF_EASY, DIFF_NORMAL = DIFF_NORMAL, DIFF_HARD = DIFF_HARD,
DIFF_ONI = DIFF_ONI, DIFF_URA = DIFF_URA,
CROWN_NONE = CROWN_NONE, CROWN_CLEAR = CROWN_CLEAR, CROWN_FC = CROWN_FC, CROWN_DFC = CROWN_DFC,
BOARD_W = BOARD_W, CAP_H = CAP_H, CEN_SRC_H = CEN_SRC_H,
CEN_CLOSED_H = CEN_CLOSED_H, CEN_OPEN_H = CEN_OPEN_H,
GLOW_CAP_H = GLOW_CAP_H, GLOW_W = GLOW_W,
SEL_BASE_BALLOON = SEL_BASE_BALLOON,
GUIDE_SELECT_CX = GUIDE_SELECT_CX, GUIDE_DECIDE_CX = GUIDE_DECIDE_CX,
GUIDE_LABEL_CY = GUIDE_LABEL_CY, GUIDE_LABEL_SPACING = GUIDE_LABEL_SPACING,
SONG_TEXT_BOX_W = SONG_TEXT_BOX_W, DS_TEXT_BOX_W = DS_TEXT_BOX_W,
BOARD_OUTLINE = BOARD_OUTLINE, BOARD_OUTLINE_BACK = BOARD_OUTLINE_BACK,
FOLDER_CLOSE_MS = FOLDER_CLOSE_MS, STALE_TIMER_MS = STALE_TIMER_MS,
SONG_CLOSE_MS = SONG_CLOSE_MS, SONG_STALE_MS = SONG_STALE_MS,
URA_LOOP = URA_LOOP, URA_LOOP_F0 = URA_LOOP_F0, URA_LOOP_LEN = URA_LOOP_LEN,
MOD_SLIDE = MOD_SLIDE, MOD_SLIDE_MS = MOD_SLIDE_MS, MOD_JSON_Y = MOD_JSON_Y,
MOD_HEADER_CY = MOD_HEADER_CY, MOD_CX = MOD_CX,
DS_SLOT_X = DS_SLOT_X, DS_BOARD_Y = DS_BOARD_Y, DS_COURSE_NAME_DY = DS_COURSE_NAME_DY,
URA_ANIM_MS = URA_ANIM_MS, URA_SWAP_MS = URA_SWAP_MS, URA_CELLS = URA_CELLS,
DS_BTN_Y = DS_BTN_Y, DS_CURSOR_Y = DS_CURSOR_Y, DS_NEIRO_Y = DS_NEIRO_Y,
DS_MARK_X = DS_MARK_X, DS_MARK_Y = DS_MARK_Y, DS_MARK_DECIDE = DS_MARK_DECIDE,
DS_MARK_DECIDE_MS = DS_MARK_DECIDE_MS,
get_any = get_any, draw_centered = draw_centered, draw_centered_b = draw_centered_b,
draw_title = draw_title, draw_guide_label = draw_guide_label,
outline = outline, board_outline = board_outline, clamp01 = clamp01,
draw_board = draw_board, draw_glow = draw_glow, glow_pulse = glow_pulse,
box_id = box_id, go_get = go_get, leg_f = leg_f, box_fade = box_fade,
leg_event = leg_event, fly_offset = fly_offset, is_decided = is_decided,
open_start_mode = open_start_mode, wheel_open_t = wheel_open_t, wheel_close_step = wheel_close_step,
folder_open_timeline = folder_open_timeline, folder_close_timeline = folder_close_timeline,
song_open_timeline = song_open_timeline, song_close_timeline = song_close_timeline,
folder_leg = folder_leg,
}
@@ -0,0 +1,34 @@
-- DanBox / other box rendering: a plain board in the arcade "dani" colour. This is the
-- `else` branch of the wheel dispatch, split out of song_select.lua.
local base = require("song_select/file_navigator/box_base")
local CEN_CLOSED_H, CEN_OPEN_H = base.CEN_CLOSED_H, base.CEN_OPEN_H
local FOLDER_CLOSE_MS, STALE_TIMER_MS = base.FOLDER_CLOSE_MS, base.STALE_TIMER_MS
local draw_board, draw_glow, glow_pulse = base.draw_board, base.draw_glow, base.glow_pulse
local draw_title = base.draw_title
local box_id = base.box_id
local box_fade, fly_offset = base.box_fade, base.fly_offset
local wheel_open_t, wheel_close_step = base.wheel_open_t, base.wheel_close_step
local folder_open_timeline, folder_close_timeline = base.folder_open_timeline, base.folder_close_timeline
function SongSelect:draw_other_board(box, state)
-- dan / other boxes: plain board in the arcade "dani" colour
local bx, by = box:box_x(), box:box_y()
local fdx, fdy = fly_offset(box); bx, by = bx + fdx, by + fdy -- wheel fly-in
local key = "o\0" .. box_id(box)
local p
if state == "open" then
local t = wheel_open_t(key)
p = t and (folder_open_timeline(t)) or 0.0
else
local e = wheel_close_step(key, FOLDER_CLOSE_MS, STALE_TIMER_MS)
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))
if p > 0 then draw_glow(self._id_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)
if p > 0 and p < 1 then draw_board(self._id_bar_genre, 10, bx, by, cen_h, fade * (1.0 - p)) end
draw_title(self:folder_title(box), bx, by - 94 * p, fade)
end
@@ -0,0 +1,172 @@
-- FolderBox rendering (genre kanban), split out of song_select.lua. Shared helpers and
-- the wheel/folder animation state come from box_base.
local base = require("song_select/file_navigator/box_base")
local folder_leg = base.folder_leg
local BOARD_W = base.BOARD_W
local CAP_H = base.CAP_H
local CEN_CLOSED_H, CEN_OPEN_H = base.CEN_CLOSED_H, base.CEN_OPEN_H
local FOLDER_CLOSE_MS, STALE_TIMER_MS = base.FOLDER_CLOSE_MS, base.STALE_TIMER_MS
local draw_board, draw_glow, glow_pulse = base.draw_board, base.draw_glow, base.glow_pulse
local draw_title = base.draw_title
local outline, board_outline = base.outline, base.board_outline
local box_id = base.box_id
local go_get, leg_f, box_fade, fly_offset = base.go_get, base.leg_f, base.box_fade, base.fly_offset
local is_decided = base.is_decided
local wheel_open_t, wheel_close_step = base.wheel_open_t, base.wheel_close_step
local folder_open_timeline, folder_close_timeline = base.folder_open_timeline, base.folder_close_timeline
-- Event/search folders are matched by display name (the engine folds DEFAULT/RECOMMENDED/
-- FAVORITE/DAN/DIFFICULTY into genre frame 0).
local EVENT_FOLDER_NAMES = { ["ドドンと!新曲祭り!"] = true, ["New Songs"] = true, ["New"] = true,
["おすすめ曲"] = true, ["Recommended"] = true, ["推薦歌曲"] = true }
local SORT_FOLDER_NAMES = { ["曲検索"] = true, ["Search Song"] = true, ["搜尋歌曲"] = true,
["むずかしさで探す"] = true, ["Difficulty Sort"] = true, ["難易度排序"] = true,
["むずかしさからえらぶ"] = true, ["Diff Sort"] = true }
local FOLDER_FRAME_EVENT, FOLDER_FRAME_SORT, FOLDER_FRAME_DANI = 11, 12, 10
local SORT_COLLECTIONS = { SEARCH = true, DIFFICULTY = true }
local EVENT_COLLECTIONS = { NEW = true, RECOMMENDED = true }
local function folder_frame(box)
local n = box.text_name
if n and EVENT_FOLDER_NAMES[n] then return FOLDER_FRAME_EVENT, true end
local col = box.collection
if col and col ~= "" then
if EVENT_COLLECTIONS[col] then return FOLDER_FRAME_EVENT, true end
if SORT_COLLECTIONS[col] then return FOLDER_FRAME_SORT, false end
end
if box.kind and box:kind() == "dan" then return FOLDER_FRAME_DANI, false end
if n and SORT_FOLDER_NAMES[n] then return FOLDER_FRAME_SORT, false end
return box:genre_frame(), false
end
-- instance_board_pattern: 960x120 strip centred on the board, y follows the title
function SongSelect:draw_pattern(bx, by, p, fade)
if not self._id_pattern then return end
tex.draw_texture(self._id_pattern, { x = bx - 480, y = by - 94 * p - 60, fade = fade })
end
-- The genre board title, re-made at the skin_config folder_box_name size.
local FOLDER_TITLE_SIZE = nil
function SongSelect:folder_title(box)
if FOLDER_TITLE_SIZE == nil then
local cfg = tex.get_skin_config("folder_box_name")
FOLDER_TITLE_SIZE = (cfg and cfg.font_size and cfg.font_size > 0) and cfg.font_size or false
end
if not FOLDER_TITLE_SIZE then return box:name() end
local key = box.text_name
if not key or key == "" then return box:name() end
local ff = folder_frame(box)
key = ff .. "\0" .. key
local t = self._folder_titles[key]
if t == nil then
t = text.create_raw_text(box.text_name, FOLDER_TITLE_SIZE, { 255, 255, 255, 255 },
board_outline(ff), false, outline(7.5), 2.0)
self._folder_titles[key] = t or false
end
return t or box:name()
end
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 -- wheel fly-in
local cen_h = CEN_CLOSED_H + (CEN_OPEN_H - CEN_CLOSED_H) * pb
local fade = box_fade(box)
local frame, has_pattern = folder_frame(box)
-- the genre board furls/unfurls horizontally across the folder transition
local ltitle = 1.0
local lf = (box:draw_state() == "open" or is_decided(box)) 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
if ba <= 0.0 then return end -- the board is not on screen
if sx < 1.0 then -- furl / unfurl: the open slab alone
draw_board(self._id_bar_folder, frame, bx, by, CEN_OPEN_H,
fade * ba, false, sx)
return
end
ch_alpha = math.min(ch_alpha, go_get("genre_chara_a", lf, 1) or 1.0)
ta = math.min(ta, go_get("genre_intro_a", lf, 1) or 1.0)
ltitle = go_get("genre_title_a", lf, 1) or 1.0
end
end
if glow_a > 0 then draw_glow(self._id_glow, bx, by, cen_h, fade * glow_a * glow_pulse()) end
-- closed folder = bar_genre, open folder = folder_graphic (title band on top)
if pb <= 0.0 then
draw_board(self._id_bar_genre, frame, bx, by, cen_h, fade)
else
draw_board(self._id_bar_folder, frame, bx, by, cen_h, fade, pb >= 1.0)
if pb < 1.0 then draw_board(self._id_bar_genre, frame, bx, by, cen_h, fade * (1.0 - pb)) end
end
if has_pattern then self:draw_pattern(bx, by, pb, fade) end
if box:has_box_texture() then
-- a library-supplied box.png is fitted (aspect kept, shrink only) into the board rect
if ch_alpha > 0 then
box:draw_box_texture({ cx = bx, cy = by,
max_w = BOARD_W, max_h = CAP_H * 2 + CEN_OPEN_H,
fade = fade * ch_alpha })
end
-- title drawn after the art, matching the cabinet's depth order
draw_title(self:folder_title(box), bx, by - 94 * pb, fade * ltitle)
return
end
-- characters: texture has L at x=510 and R at x=1410
if ch_alpha > 0 then
local slide = 450.0 - ch_off
local gframe = box:genre_frame()
tex.draw_texture(self._id_box_chara, { frame = gframe, x = bx - 960 + slide, y = by - 240, x2 = 960 - 1920, fade = fade * ch_alpha,
src = { x = 0, y = 0, width = 960, height = 480 } })
tex.draw_texture(self._id_box_chara, { frame = gframe, x = bx - slide, y = by - 240, x2 = 960 - 1920, fade = fade * ch_alpha,
src = { x = 960, y = 0, width = 960, height = 480 } })
end
-- title rises to y-94 when open; the song count stands in for the intro text
draw_title(self:folder_title(box), bx, by - 94 * pb, fade * ltitle)
local n = box.tja_count
if ta > 0 and n and n > 0 then
local ckey = frame .. "\0" .. n
local label = self._count_cache[ckey]
if not label then
label = text.create_raw_text(tostring(n) .. " songs", 32, { 255, 255, 255, 255 }, board_outline(frame), false, outline(4.0), 2.0)
self._count_cache[ckey] = label
end
if label then draw_title(label, bx, by + 49 * pb, fade * ta) end
end
end
function SongSelect:draw_folder_closed(box)
local key = "f\0" .. box_id(box)
local e = wheel_close_step(key, FOLDER_CLOSE_MS, STALE_TIMER_MS)
if e then
local pb, ch_alpha, ch_off, ta = folder_close_timeline(e)
self:draw_folder_board(box, pb, ch_alpha, ch_off, ta, pb)
return
end
self:draw_folder_board(box, 0.0, 0.0, 340.0, 0.0, 0.0)
end
function SongSelect:draw_folder_open(box)
-- remember the decided board: the engine removes it from the wheel while its songs
-- load, and draw_overlays keeps drawing it until it has furled away
folder_leg.last_open_id, folder_leg.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)
local t = wheel_open_t(key)
if t == nil then -- 508 ms pre-growth hold: fully closed
self:draw_folder_board(box, 0.0, 0.0, 340.0, 0.0, 0.0)
return
end
local pb, ch_alpha, ch_off, ta = folder_open_timeline(t)
self:draw_folder_board(box, pb, ch_alpha, ch_off, ta, pb)
end
return { folder_frame = folder_frame }
@@ -0,0 +1,426 @@
-- SongBox rendering (song board, difficulty plates, badges, diff select), split out of
-- song_select.lua. All shared helpers/state come from box_base.
local base = require("song_select/file_navigator/box_base")
local folder_leg = base.folder_leg
local DIFF_BACK, DIFF_MODIFIER = base.DIFF_BACK, base.DIFF_MODIFIER
local DIFF_EASY, DIFF_NORMAL, DIFF_HARD, DIFF_ONI, DIFF_URA =
base.DIFF_EASY, base.DIFF_NORMAL, base.DIFF_HARD, base.DIFF_ONI, base.DIFF_URA
local CROWN_NONE, CROWN_FC, CROWN_DFC = base.CROWN_NONE, base.CROWN_FC, base.CROWN_DFC
local BOARD_W = base.BOARD_W
local CEN_CLOSED_H, CEN_OPEN_H = base.CEN_CLOSED_H, base.CEN_OPEN_H
local SONG_TEXT_BOX_W, DS_TEXT_BOX_W = base.SONG_TEXT_BOX_W, base.DS_TEXT_BOX_W
local SONG_CLOSE_MS, SONG_STALE_MS = base.SONG_CLOSE_MS, base.SONG_STALE_MS
local URA_LOOP, URA_LOOP_F0, URA_LOOP_LEN = base.URA_LOOP, base.URA_LOOP_F0, base.URA_LOOP_LEN
local DS_SLOT_X, DS_BOARD_Y, DS_COURSE_NAME_DY = base.DS_SLOT_X, base.DS_BOARD_Y, base.DS_COURSE_NAME_DY
local URA_ANIM_MS, URA_SWAP_MS, URA_CELLS = base.URA_ANIM_MS, base.URA_SWAP_MS, base.URA_CELLS
local DS_BTN_Y, DS_CURSOR_Y, DS_NEIRO_Y = base.DS_BTN_Y, base.DS_CURSOR_Y, base.DS_NEIRO_Y
local DS_MARK_X, DS_MARK_Y = base.DS_MARK_X, base.DS_MARK_Y
local DS_MARK_DECIDE, DS_MARK_DECIDE_MS = base.DS_MARK_DECIDE, base.DS_MARK_DECIDE_MS
local get_any = base.get_any
local draw_centered, draw_centered_b = base.draw_centered, base.draw_centered_b
local draw_title = base.draw_title
local outline, board_outline = base.outline, base.board_outline
local draw_board, draw_glow, glow_pulse = base.draw_board, base.draw_glow, base.glow_pulse
local box_id = base.box_id
local go_get, leg_f, box_fade, fly_offset = base.go_get, base.leg_f, base.box_fade, base.fly_offset
local wheel_open_t, wheel_close_step = base.wheel_open_t, base.wheel_close_step
local song_open_timeline, song_close_timeline = base.song_open_timeline, base.song_close_timeline
local SONG_TITLE_OUTLINE = 5.0
local SONG_TITLE_SIZE = nil -- resolved once from skin_config song_box_name
local SONG_TITLE_CACHE_MAX = 128 -- bound the per-title texture cache
-- crown / rank badges (72x72) at arcade positions, one column per player.
local ICON_ARTS = nil -- name -> texture id (resolved lazily)
function SongSelect:song_icon(box)
if ICON_ARTS == nil then ICON_ARTS = { icon_new = self._id_new_icon } end
local list = {}
if box.is_new then list[#list + 1] = "icon_new" end
if #list == 0 then return nil end
local pick = list[(math.floor(tex.get_current_ms() / 1000.0) % #list) + 1]
return ICON_ARTS[pick]
end
function SongSelect:badge_diffs(box, pl)
local sel = self._mark_diff and self._mark_diff[pl]
local crown_d, rank_d
if sel and sel >= DIFF_EASY and sel <= DIFF_URA then
local info = box:course_info(sel)
if info.has_course then
if info.crown ~= CROWN_NONE then crown_d = sel end
if (info.rank or 0) > 0 then rank_d = sel end
end
end
if not crown_d or not rank_d then
local cbest, rbest
for d = DIFF_EASY, DIFF_URA do
local info = box:course_info(d)
if info.has_course then
if info.crown ~= CROWN_NONE then cbest = d end
if (info.rank or 0) > 0 then rbest = d end
end
end
crown_d = crown_d or cbest
rank_d = rank_d or rbest
end
return crown_d, rank_d
end
function SongSelect:draw_badges(box, bx, by, p, fade)
local scale = 0.75 + 0.25 * p
local size = 72 * scale
local players = math.min(2, tex.player_num())
for pl = 1, players do
local cx = bx + ((pl == 1) and -425 or 425)
local crown_d, rank_d = self:badge_diffs(box, pl)
if crown_d then
local info = box:course_info(crown_d)
local id = (info.crown == CROWN_DFC) and self._id_crown_dfc
or (info.crown == CROWN_FC) and self._id_crown_fc or self._id_crown_clear
local cy = by + (-27 + (-114 + 27) * p)
draw_centered(id, cx, cy, size, size, { frame = crown_d, x2 = size - 72, y2 = size - 72, fade = fade })
end
if rank_d then
local info = box:course_info(rank_d)
local cy = by + (27 + (-42 - 27) * p)
draw_centered(self._id_score_rank, cx, cy, size, size,
{ frame = (info.rank - 1) * 5 + rank_d, x2 = size - 72, y2 = size - 72, fade = fade })
end
end
end
-- SongBox
-- pb = board progress (0..1), ia = contents alpha, ic = NEW-icon alpha.
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 -- wheel fly-in
local fade = math.min(1.0, box_fade(box))
-- folder-ENTER/EXIT unfurl/furl: while the slab's sx < 1 only the slab is drawn,
-- and the contents alphas come from the clip's own channels.
local gof = leg_f()
local ltitle = 1.0
if gof and folder_leg.kind == "open" and gof < folder_leg.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
if ba <= 0.0 then return end -- board not on screen on this frame
if sx < 1.0 then -- unfurl / furl: the slab alone
draw_board(self._id_bar_genre, box:genre_frame(), bx, by, CEN_OPEN_H,
fade * ba, nil, sx)
return
end
local ia2 = go_get("song_info_a", gof, 1)
if ia2 then ia = math.min(ia or 1.0, ia2) end
ltitle = go_get("song_title_a", gof, 1) or 1.0
local ic2 = go_get("song_icon_a", gof, 1)
if ic2 then ic = math.min(ic or 0.0, ic2) end
end
end
local cen_h = CEN_CLOSED_H + (CEN_OPEN_H - CEN_CLOSED_H) * pb
if pb > 0 then draw_glow(self._id_glow, bx, by, cen_h, fade * pb * glow_pulse()) end
draw_board(self._id_bar_genre, box:genre_frame(), bx, by, cen_h, fade)
if pb <= 0.0 then
draw_title(self:song_title(box), bx, by, fade, SONG_TEXT_BOX_W, 0)
self:draw_badges(box, bx, by, 0.0, fade)
-- instance_icon hangs off the board's left edge (-532)
local icon = (ic > 0) and self:song_icon(box) or nil
if icon then
draw_centered(icon, bx - 532, by + 4, 120, 96, { fade = fade * ic })
end
return
end
local cfade = fade * ia
if cfade > 0 then
self:draw_plates(box, bx, by, cfade)
self:draw_badges(box, bx, by, pb, cfade)
end
-- title rises from the closed position (y=0) to the open one (y=-70) with the growth
draw_title(self:song_title(box), bx, by - 70 * pb, fade * ltitle, SONG_TEXT_BOX_W, 0)
local sub = self:song_subtitle(box)
if sub and cfade > 0 then draw_title(sub, bx, by - 28, cfade, SONG_TEXT_BOX_W) end
if tex.display_bpm() and cfade > 0 then
local bpm_cfg = tex.get_skin_config("song_box_bpm")
box:bpm_text():draw({ x = bx + bpm_cfg.x, y = by + bpm_cfg.y, fade = cfade })
end
local icon = (ic > 0) and self:song_icon(box) or nil
if icon then
draw_centered(icon, bx - 532, by + 4, 120, 96, { fade = fade * ic })
end
-- 新音 / 旧音 / 期間限定 marker, hung off the board's right edge (mirror of the icon)
local ok_exd, exd = pcall(function() return box:ex_data_flag() end)
if ok_exd and exd and exd > 0 and cfade > 0 then
local art = (exd == 1) and get_any("yellow_box", "ex_data_new_audio")
or (exd == 2) and get_any("yellow_box", "ex_data_old_audio")
or get_any("yellow_box", "ex_data_limited_time")
if art then
local s = tex.get_screen_scale()
draw_centered_b(art, { 458 * s, 120 * s }, bx + 532, by + 4,
224 * s, 64 * s, { fade = cfade })
end
end
if box.is_favorite and cfade > 0 then
local fav = get_any("yellow_box", "favorite_" .. tex.player_num() .. "p")
if fav then tex.draw_texture(fav, { x = bx - 480, y = by - 176, fade = cfade }) end
end
end
function SongSelect:draw_song_closed(box)
local key = "s\0" .. box_id(box) -- per-BOX, not per-title
local e = wheel_close_step(key, SONG_CLOSE_MS, SONG_STALE_MS)
if e then
self:draw_song_board(box, song_close_timeline(e))
return
end
self:draw_song_board(box, 0.0, 0.0, 1.0)
end
-- Diff plates row (song_kanban_info, board y +75): plate 184x96 with the course name
-- under the icon, a star and the level number.
function SongSelect:chip_label(diff)
local lbl = self._chip_labels[diff]
if lbl == nil then
local key = "chip_course_name_" .. diff
if tex.get_skin_config(key) then
lbl = text.create_text(key, { 255, 255, 255, 255 }, { 40, 20, 20, 255 }, false, outline(1.5), 1.0)
end
self._chip_labels[diff] = lbl or false
end
return lbl or nil
end
function SongSelect:draw_plates(box, bx, by, fade)
local has_ura = box:has_ura()
local ura_weight = 0.0
local oni_alpha = 1.0
if has_ura then
local f = URA_LOOP_F0 + (tex.get_current_ms() * 0.06) % URA_LOOP_LEN
ura_weight = URA_LOOP:get("ura/text_course_ura", f, "a") or 0.0
oni_alpha = URA_LOOP:get("oni/text_course_oni", f, "a") or 1.0
end
local py = by + 75
local function plate(cx, diff, f)
local info = box:course_info(diff)
if not info.has_course or f <= 0 then return end
draw_centered(self._id_plate, cx, py, 184, 96, { frame = diff, fade = f })
draw_centered(self._id_star, cx + 27, py, 40, 40, { frame = diff, fade = f })
local lv = math.max(1, math.min(11, info.level))
draw_centered(self._id_level_number, cx + 61, py, 48, 48, { frame = diff * 11 + (lv - 1), fade = f })
local name = self:chip_label(diff)
if name then draw_title(name, cx - 42, py + 30, f) end
if info.is_branching then
draw_centered(self._id_branch, cx - 69, py - 23, 40, 40, { fade = f })
end
end
-- charted columns: easy / normal / hard / (oni|ura)
local cols = {}
for d = 0, 2 do if box:course_info(d).has_course then cols[#cols + 1] = d end end
local has_oni = box:course_info(DIFF_ONI).has_course
if has_oni or has_ura then cols[#cols + 1] = DIFF_ONI end
local n = #cols
for i, d in ipairs(cols) do
local cx = bx + (i - 1 - (n - 1) / 2) * 182
if d == DIFF_ONI and has_ura then
if has_oni then
plate(cx, DIFF_ONI, fade * oni_alpha) -- oni below, ura on top
plate(cx, DIFF_URA, fade * ura_weight)
else
plate(cx, DIFF_URA, fade)
end
else
plate(cx, d, fade)
end
end
return ura_weight
end
function SongSelect:draw_song_open(box)
local key = "s\0" .. box_id(box)
local t = wheel_open_t(key)
if t == nil then -- 508 ms pre-growth hold: fully closed
self:draw_song_board(box, 0.0, 0.0, 1.0)
return
end
self:draw_song_board(box, song_open_timeline(t))
end
-- Difficulty select
function SongSelect:draw_diff_mark_decided(p, dec, fade)
local f = math.floor((tex.get_current_ms() - dec.t0) / DS_MARK_DECIDE_MS) + 1
local k = DS_MARK_DECIDE[math.min(f, #DS_MARK_DECIDE)]
local id, id_w = self._id_mark[dec.course], self._id_mark_w[dec.course]
local cx, cy = DS_MARK_X[p], DS_MARK_Y
local function stretched(tid, a, sy)
if a <= 0 then return end
tex.draw_texture(tid, { x = cx, y = cy, y2 = 680 * (sy - 1), origin = { 340, 340 * sy }, fade = a * fade })
end
stretched(id_w, k[5], 1) -- mark_course_white_2: backing
stretched(id, k[1], k[2]) -- mark_course
stretched(id_w, k[3], k[4]) -- mark_course_white: the flash on top
end
function SongSelect:draw_song_diff_select_bg(box)
local fade = box.diff_fade_in.attribute
for p = 1, 2 do
local d = self._mark_diff[p]
local dec = self._mark_decided[p]
if dec and self._id_mark[dec.course] then
self:draw_diff_mark_decided(p, dec, fade)
elseif self._id_diff_mark and d and d >= DIFF_EASY and DS_MARK_X[p] then
draw_centered(self._id_diff_mark, DS_MARK_X[p], DS_MARK_Y, 680, 680, { frame = d, fade = fade })
end
end
tex.draw_texture(self._id_backboard, { frame = box:genre_frame(), x = 960 - 636, y = 440 - 392, fade = fade })
end
function SongSelect:draw_song_diff_select(box)
folder_leg.diff_seen_ms = tex.get_current_ms()
local fade = box.diff_fade_in.attribute
local is_ura = box.is_ura
local side = (tex.player_num() == 2) and 1 or -1
local now = tex.get_current_ms()
-- oni<->ura toggle detection
if fade < 1.0 then
self._ura_prev = is_ura; self._ura_anim = nil
elseif self._ura_prev ~= nil and is_ura ~= self._ura_prev then
self._ura_anim = { t0 = now, to_ura = is_ura }
end
self._ura_prev = is_ura
local ura_elapsed = self._ura_anim and (now - self._ura_anim.t0) or nil
if ura_elapsed and ura_elapsed >= URA_ANIM_MS then self._ura_anim = nil; ura_elapsed = nil end
local ura_mid = (not ura_elapsed) or ura_elapsed >= URA_SWAP_MS
-- what the oni slot shows: before the swap point the previous course, after it the new one
local oni_shown = is_ura
if ura_elapsed and not ura_mid then oni_shown = not is_ura end
-- buttons: return + a small option wrench (the engine cursor can still reach MODIFIER)
draw_centered(self._id_btn_back, DS_SLOT_X[DIFF_BACK], DS_BTN_Y, 128, 128, { fade = fade })
draw_centered(self._id_btn_option, DS_SLOT_X[DIFF_MODIFIER], DS_BTN_Y, 128, 128, { fade = fade })
-- diff boards
for i = 0, 3 do
local cx = DS_SLOT_X[i]
local diff = (i == DIFF_ONI and oni_shown) and DIFF_URA or i
local info = box:course_info(diff)
local f = info.has_course and fade or math.min(fade, 0.4)
draw_centered(self._id_diff_board, cx, DS_BOARD_Y, 200, 360, { frame = diff, fade = f })
if info.has_course and not (i == DIFF_ONI and ura_elapsed) then
local crown = info.crown or 0
draw_centered(self._id_small_crown, cx + side * 58, DS_BOARD_Y - 138, 40, 40, { frame = crown, fade = f })
if (info.rank or 0) > 0 then
draw_centered(self._id_small_rank, cx + side * 20, DS_BOARD_Y - 138, 40, 40, { frame = info.rank - 1, fade = f })
end
local lv = math.max(1, math.min(11, info.level))
tex.draw_texture(self._id_small_star, { frame = 0, x = cx - 40, y = DS_BOARD_Y + 66, fade = f })
tex.draw_texture(self._id_small_star, { frame = lv, x = cx - 4, y = DS_BOARD_Y + 68, fade = f })
draw_centered(self._id_dot_bar, cx, DS_BOARD_Y + 116, 172, 24, { fade = f })
for k = 0, math.min(10, info.level) - 1 do
draw_centered(self._id_dot_star, cx - 67 + k * 15, DS_BOARD_Y + 116, 24, 24, { fade = f })
end
if info.is_branching then
draw_centered(self._id_branch_diff, cx, DS_BOARD_Y + 144, 40, 40, { fade = f })
end
end
end
-- change animation cells over the oni slot
if ura_elapsed then
local id = self._ura_anim.to_ura and self._id_ura_to_ura or self._id_ura_to_oni
if id then
local cell = math.min(URA_CELLS - 1, math.floor(ura_elapsed / URA_ANIM_MS * URA_CELLS))
tex.draw_texture(id, { frame = cell, x = DS_SLOT_X[DIFF_ONI] - 170, y = DS_BOARD_Y - 210, fade = fade })
end
end
for i = 0, 3 do
local diff = (i == DIFF_ONI and oni_shown) and DIFF_URA or i
local info = box:course_info(diff)
if info.has_course then
local lbl = self:course_label(diff)
if lbl then draw_title(lbl, DS_SLOT_X[i], DS_BOARD_Y + DS_COURSE_NAME_DY, fade) end
end
end
-- title / subtitle inside the backboard header
draw_title(self:header_title(box), 960, 178, fade, DS_TEXT_BOX_W, 0)
draw_title(self:header_subtitle(box), 960, 242, fade, DS_TEXT_BOX_W, 0)
end
function SongSelect:song_subtitle(box)
local str = box.text_subtitle
if not str or str == "" then return nil end
local gf = self._inside_frame or box:genre_frame()
local key = gf .. "\0" .. str
if self._sub_key ~= key then
self._sub_key = key
self._sub_label = text.create_raw_text(str, 24, { 255, 255, 255, 255 }, board_outline(gf), false, outline(3.5), 2.0)
end
return self._sub_label
end
-- Song board title, arcade-exact: font from skin_config song_box_name, black outline,
-- cached per title (the cache is dropped wholesale past SONG_TITLE_CACHE_MAX).
function SongSelect:song_title(box)
local name = box.text_name
if not name or name == "" then return box:name() end
local gf = self._inside_frame or box:genre_frame()
local key = gf .. "\0" .. name -- the outline follows the genre, so it keys the cache
local t = self._song_titles[key]
if t == nil then
if SONG_TITLE_SIZE == nil then
local cfg = tex.get_skin_config("song_box_name")
SONG_TITLE_SIZE = (cfg and cfg.font_size and cfg.font_size > 0) and cfg.font_size or 42
end
if self._song_titles_n >= SONG_TITLE_CACHE_MAX then
self._song_titles = {}
self._song_titles_n = 0
end
t = text.create_raw_text(name, SONG_TITLE_SIZE, { 255, 255, 255, 255 }, board_outline(gf),
false, outline(SONG_TITLE_OUTLINE), 2.0)
self._song_titles[key] = t or false
self._song_titles_n = self._song_titles_n + 1
end
return t or box:name()
end
function SongSelect:header_title(box)
local name = box.text_name or ""
if self._header_title_key ~= name then
self._header_title_key = name
self._header_title = text.create_raw_text(name, 48, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, outline(7.0), 2.0)
end
return self._header_title
end
function SongSelect:header_subtitle(box)
local sub = box.text_subtitle
if sub == nil then return box:subtitle_large() end -- engine without the binding
if sub == "" then return nil end
if self._header_sub_key ~= sub then
self._header_sub_key = sub
self._header_sub = text.create_raw_text(sub, 30, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, outline(4.0), 2.0)
end
return self._header_sub
end
function SongSelect:course_label(diff)
local lbl = self._course_labels[diff]
if lbl == nil then
local key = "course_name_" .. diff
if tex.get_skin_config(key) then
lbl = text.create_text(key, { 255, 255, 255, 255 }, { 20, 20, 20, 255 }, false, outline(4.5), 1.0)
end
self._course_labels[diff] = lbl or false
end
return lbl or nil
end
+188
View File
@@ -0,0 +1,188 @@
-- 演奏オプション / 音色 board — replaces ModifierSelector::draw() / NeiroSelector::draw().
-- Same texture.json bases and skin_config keys the C++ board used, so geometry is exact.
-- Split out of song_select.lua.
local base = require("song_select/file_navigator/box_base")
local get_any = base.get_any
local outline = base.outline
local OPT_CURSOR_MS = 1000 -- cursor loop = 60 frames @ 60 fps
local OPT_CURSOR_MIN = 51 / 256 -- its alpha floor
local OPT_GREY = { 166, 168, 171, 255 }
local OPT_CHANGED = { 255, 255, 0, 255 }
local OPT_SCRIM = { 0, 0, 0, 128 }
local OPT_ARROW_NUDGE = 5
function SongSelect:_opt_ids()
if self._opt_ids_t then return self._opt_ids_t end
-- Must load these subsets here: script_manager.tex is its own TextureWrapper, so a
-- subset the engine loaded elsewhere is not resident in the Lua one.
tex.load_folder("song_select", "modifier")
tex.load_folder("song_select", "neiro")
local t = {}
t.top = get_any("modifier", "top")
t.bottom = get_any("modifier", "bottom")
t.bg = get_any("modifier", "background")
t.row = get_any("modifier", "mod_bg")
t.row_hl = get_any("modifier", "mod_bg_highlight")
t.box = get_any("modifier", "mod_box")
t.arrow = get_any("modifier", "blue_arrow")
t.p = { get_any("modifier", "1p"), get_any("modifier", "2p") }
t.icon = {
auto = get_any("modifier", "mod_auto"),
speed = get_any("modifier", "mod_baisaku"),
sanbai = get_any("modifier", "mod_sanbai"),
yonbai = get_any("modifier", "mod_yonbai"),
speed_by_value = {
[11] = get_any("modifier", "mod_speed_x1_1"), [12] = get_any("modifier", "mod_speed_x1_2"),
[13] = get_any("modifier", "mod_speed_x1_3"), [14] = get_any("modifier", "mod_speed_x1_4"),
[15] = get_any("modifier", "mod_speed_x1_5"), [16] = get_any("modifier", "mod_speed_x1_6"),
[17] = get_any("modifier", "mod_speed_x1_7"), [18] = get_any("modifier", "mod_speed_x1_8"),
[19] = get_any("modifier", "mod_speed_x1_9"), [20] = get_any("modifier", "mod_speed_x2"),
[25] = get_any("modifier", "mod_speed_x2_5"), [30] = get_any("modifier", "mod_speed_x3"),
[35] = get_any("modifier", "mod_speed_x3_5"), [40] = get_any("modifier", "mod_speed_x4"),
},
display = get_any("modifier", "mod_doron"),
inverse = get_any("modifier", "mod_abekobe"),
kimagure = get_any("modifier", "mod_kimagure"),
detarame = get_any("modifier", "mod_detarame"),
}
self._opt_ids_t = t
return t
end
-- Row label / value strings as OutlinedText, cached by content.
function SongSelect:_opt_text(s)
if s == nil or s == "" then return nil end
if not self._opt_texts then
self._opt_texts = {}
local cfg = tex.get_skin_config("modifier_text")
self._opt_font = (cfg and cfg.font_size ~= 0 and cfg.font_size) or 26
end
local t = self._opt_texts[s]
if t == nil then
t = text.create_raw_text(s, self._opt_font, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, outline(3.0), 2.0) or false
self._opt_texts[s] = t
end
return t or nil
end
-- Arcade cursor pulse, sampled from anim/option_cursor; hand triangle is the fallback.
local option_cursor_ok, option_cursor_anim = pcall(require, "anim/sampler")
local OPT_CURSOR_CLIP = (option_cursor_ok and option_cursor_anim.safe("anim/option_cursor"))
or { get = function() return nil end, first = function() return 0 end,
last = function() return 0 end }
local OPT_CURSOR_LEN = math.max(1, OPT_CURSOR_CLIP:last() - OPT_CURSOR_CLIP:first() + 1)
local function opt_cursor_alpha(ms)
local f = (ms * 0.06) % OPT_CURSOR_LEN
local a = OPT_CURSOR_CLIP:get("#3@0", f, "a")
if a then return a end
local u = (ms % OPT_CURSOR_MS) / OPT_CURSOR_MS
local tri = (u < 0.5) and (u * 2) or ((1 - u) * 2)
return 1.0 - (1.0 - OPT_CURSOR_MIN) * tri
end
-- One board. `rows` = {label, value, changed, greyed, name}; `cursor` = 1-based row (nil = none).
function SongSelect:draw_option_board(player, offset, rows, cursor, change)
local ids = self:_opt_ids()
if not ids.top or not ids.row or not ids.box then return end
local p = player:player_num()
local p2cfg = tex.get_skin_config("option_p2")
local x = (p - 1) * ((p2cfg and p2cfg.x) or 1490)
local pitch = tex.get_skin_config("modifier_offset")
pitch = (pitch and pitch.y ~= 0 and pitch.y) or 61
local tcfg = tex.get_skin_config("modifier_text")
local ocfg = tex.get_skin_config("modifier_offset_2")
local text_x, text_y = (tcfg and tcfg.x) or 300, (tcfg and tcfg.y) or 1178
local name_x = (ocfg and ocfg.x) or 44
local arrow_span = (ocfg and ocfg.y) or 140
local n = #rows
local ms = tex.get_current_ms()
tex.draw_texture(ids.top, { x = x, y = offset })
if ids.p[p] then tex.draw_texture(ids.p[p], { x = x, y = offset }) end
if ids.bottom then tex.draw_texture(ids.bottom, { x = x, y = offset + n * pitch }) end
for i = 1, n do
local row = rows[i]
local row_y = offset + (i - 1) * pitch
local is_cur = (cursor == i)
if ids.bg then tex.draw_texture(ids.bg, { x = x, y = row_y }) end
tex.draw_texture(ids.row, { x = x, y = row_y })
if row.greyed then
-- arcade "grayout": flat grey laid on top of the row plate
if ids.row_hl then tex.draw_texture(ids.row_hl, { x = x, y = row_y, color = OPT_GREY }) end
elseif is_cur and ids.row_hl then
tex.draw_texture(ids.row_hl, { x = x, y = row_y, fade = opt_cursor_alpha(ms) })
end
tex.draw_texture(ids.box, { x = x, y = row_y, color = row.changed and OPT_CHANGED or nil })
local label = self:_opt_text(row.label)
if label then label:draw({ x = name_x + x, y = text_y + row_y }) end
local value = self:_opt_text(row.value)
if value then
value:draw({ x = math.floor(text_x - value.width / 2 + 0.5) + x, y = text_y + row_y })
end
-- engine row icon; state is the raw value so it never depends on the localised string
local icon
local st = row.state or 0
if row.name == "speed" then
if st > 10 then
local best
for v, id in pairs(ids.icon.speed_by_value or {}) do
if id and v <= st and (not best or v > best) then best = v end
end
icon = best and ids.icon.speed_by_value[best] or nil
end
if not icon then
if st >= 40 then icon = ids.icon.yonbai
elseif st >= 30 then icon = ids.icon.sanbai
elseif st > 10 then icon = ids.icon.speed end
end
elseif row.name == "random" then
if st == 2 then icon = ids.icon.detarame
elseif st == 1 then icon = ids.icon.kimagure end
elseif row.changed then
icon = ids.icon[row.name]
end
if icon then tex.draw_texture(icon, { x = x, y = row_y }) end
if is_cur and ids.arrow then
local nl, nr = 0, 0
if change and change.active then
local amp = OPT_ARROW_NUDGE * math.max(0, math.min(1, change.fade))
if change.dir > 0 then nl = amp else nr = amp end
end
tex.draw_texture(ids.arrow, { x = x - nl, y = row_y })
tex.draw_texture(ids.arrow, { mirror = "horizontal", x = x + arrow_span + nr, y = row_y })
end
if row.greyed then tex.draw_texture(ids.row, { x = x, y = row_y, color = OPT_SCRIM }) end
end
end
-- Engine entry point. kind: 1 = 演奏オプション, 2 = the standalone 音色 panel.
function SongSelect:draw_option_panel(player, kind)
if kind == 1 then
local rows = player.modifier_rows and player:modifier_rows()
local off = player.modifier_offset and player:modifier_offset()
if not rows or not off then return end
local cursor = player.modifier_index and player:modifier_index() or nil
if cursor and (cursor < 1 or cursor > #rows) then cursor = nil end
if player.modifier_confirmed and player:modifier_confirmed() then cursor = nil end
self._mod_off[player:player_num()] = off
self:draw_option_board(player, off, rows, cursor,
player.modifier_change and player:modifier_change())
else
local names = player.neiro_names and player:neiro_names()
local off = player.neiro_offset and player:neiro_offset()
if not names or not off then return end
local idx = (player.neiro_index and player:neiro_index()) or 1
local rows = { { name = "neiro", state = idx - 1, value = names[idx] or "",
changed = (idx ~= 1), greyed = false } }
self:draw_option_board(player, off, rows, 1, nil)
end
end
+70
View File
@@ -0,0 +1,70 @@
-- SongSelectPlayer selector cursor (bubble + course frame / button glow), split out of
-- song_select.lua. Drawn in two passes (under/over the wheel).
local base = require("song_select/file_navigator/box_base")
local DIFF_EASY, DIFF_NEIRO, DIFF_ONI = base.DIFF_EASY, base.DIFF_NEIRO, base.DIFF_ONI
local DS_SLOT_X = base.DS_SLOT_X
local DS_BOARD_Y, DS_CURSOR_Y, DS_NEIRO_Y, DS_BTN_Y = base.DS_BOARD_Y, base.DS_CURSOR_Y, base.DS_NEIRO_Y, base.DS_BTN_Y
local SEL_BASE_BALLOON = base.SEL_BASE_BALLOON
local MOD_SLIDE, MOD_SLIDE_MS = base.MOD_SLIDE, base.MOD_SLIDE_MS
local draw_centered, draw_centered_b = base.draw_centered, base.draw_centered_b
function SongSelect:draw_selector(player, is_half, fade_in, pass)
local under = (pass == nil) or (pass == 0)
local over = (pass == nil) or (pass == 1)
local diff = player:selected_difficulty()
local p = player:player_num()
if under then
self._mark_diff[p] = diff
local decided = diff >= DIFF_EASY and player.difficulty_decided and player:difficulty_decided()
if decided then
if not self._mark_decided[p] then
self._mark_decided[p] = { t0 = tex.get_current_ms(), course = diff }
end
else
self._mark_decided[p] = nil
end
end
if fade_in < 1.0 then return end
local mod_on = player:modifier_active()
if player.modifier_offset then
self._mod_off[p] = player:modifier_offset()
elseif mod_on then
if not self._mod_seen[p] then
local now = tex.get_current_ms()
local prev = self._last_sel_ms or now
self._mod_t0[p] = (now - prev < 250) and math.min(now, prev + 16.7) or now
end
local u = math.max(0.0, math.min(1.0, (tex.get_current_ms() - self._mod_t0[p]) / MOD_SLIDE_MS))
self._mod_off[p] = -MOD_SLIDE * u * (2 - u)
else
self._mod_off[p] = nil
end
self._last_sel_ms = tex.get_current_ms()
self._mod_seen[p] = mod_on
local fade = (player:neiro_active() or mod_on) and 0.5 or fade_in
-- the cursor offset is +-40 only while both players sit on the same slot
local cursor_dx = is_half and ((p == 2) and 40 or -40) or 0
local sx = DS_SLOT_X[(diff >= DIFF_EASY) and math.min(DIFF_ONI, diff) or diff]
if not sx then return end
if diff >= DIFF_EASY then
local col = math.min(DIFF_ONI, diff)
if col == DIFF_ONI and self._ura_anim then return end -- cursor/frame hidden during the change
sx = DS_SLOT_X[col]
local frame_id = (p == 1) and self._id_course_frame or self._id_course_frame2
if under then draw_centered(frame_id, sx, DS_BOARD_Y, 248, 408, { fade = fade }) end
if over then
draw_centered_b((p == 1) and self._id_bubble_1p or self._id_bubble_2p, SEL_BASE_BALLOON,
sx + cursor_dx, DS_CURSOR_Y, 124, 124, { fade = fade })
end
else
local by = (diff == DIFF_NEIRO) and DS_NEIRO_Y or DS_BTN_Y
if under then draw_centered(self._id_btn_glow, sx, by, 168, 168, { fade = fade }) end
if over then
draw_centered_b((p == 1) and self._id_bubble_1p or self._id_bubble_2p, SEL_BASE_BALLOON,
sx + cursor_dx, DS_CURSOR_Y, 124, 124, { fade = fade })
end
end
end
File diff suppressed because it is too large. Load diff
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more