data: add Watcha Gatcha charts from official master
Publish GitHub Pages output / publish (push) Successful in 36s

This commit is contained in:
2026-09-27 06:40:12 +09:00
parent 7c7f0d40c0
commit b87c5d2e18
20 changed files with 402 additions and 43 deletions
+1
View File
@@ -11,6 +11,7 @@ data/*
!data/.gitkeep !data/.gitkeep
!data/card-overrides.csv !data/card-overrides.csv
!data/score-calibrations.json !data/score-calibrations.json
!data/song-metadata-overrides.json
!data/holodoridb/ !data/holodoridb/
!data/holodoridb/** !data/holodoridb/**
data/holodoridb/*.tmp data/holodoridb/*.tmp
+7 -1
View File
@@ -1,7 +1,13 @@
--- ---
updatedAt: 2026-09-25T14:08:52Z updatedAt: 2026-09-26T12:00:00Z
--- ---
## 2026년 9월 26일
### Watcha Gatcha!!!!!!!! 채보 추가
`NEW` 「Watcha Gatcha!!!!!!!!」의 Easy·Normal·Hard·Expert 채보를 추가했습니다. 게임 내 확인 난이도는 6·13·22·27이며 곡 길이는 122초입니다. 노트와 스페셜 발동 시각은 원본 채보를 사용합니다. 곡별 점수 계수와 가창 멤버 정보를 반영하여 점수 공식과 가창 멤버 보너스를 적용합니다.
## 2026년 9월 25일 ## 2026년 9월 25일
### 배치 분석 빈도 조합 비교 ### 배치 분석 빈도 조합 비교
+10 -10
View File
@@ -1,24 +1,24 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-09-19T01:15:22.929Z", "generatedAt": "2026-09-26T21:34:40.634Z",
"sources": { "sources": {
"eng": { "eng": {
"repository": "https://github.com/HolodoriDB/holodori-db-eng-diff", "repository": "https://github.com/HolodoriDB/holodori-db-eng-diff",
"commit": "f7428d4092d57b34311f030e00ff46e7dbbc29e8", "commit": "7f69b2404abe3001ce411240ed2323a6313a2708",
"commitDate": "2026-09-18T10:50:49Z", "commitDate": "2026-09-26T16:03:09Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
}, },
"kor": { "kor": {
"repository": "https://github.com/HolodoriDB/holodori-db-kor-diff", "repository": "https://github.com/HolodoriDB/holodori-db-kor-diff",
"commit": "5517e40b931fec10253f5b850989346518e233c2", "commit": "98f8b557a095c837b51126d993c34d582fb5c5f2",
"commitDate": "2026-09-18T13:17:53Z", "commitDate": "2026-09-26T13:30:40Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
}, },
"jpn": { "jpn": {
"repository": "https://github.com/HolodoriDB/holodori-db-jpn-diff", "repository": "https://github.com/HolodoriDB/holodori-db-jpn-diff",
"commit": "a22c8e29a981a8987bf2074c245e2fc40ad4dbea", "commit": "e43f062c32ff4e04567235efcd58448cd6b10f35",
"commitDate": "2026-09-18T11:03:55Z", "commitDate": "2026-09-26T16:18:53Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
} }
}, },
"tables": { "tables": {
+10 -10
View File
@@ -1,24 +1,24 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-09-19T01:15:22.929Z", "generatedAt": "2026-09-26T21:34:40.634Z",
"sources": { "sources": {
"eng": { "eng": {
"repository": "https://github.com/HolodoriDB/holodori-db-eng-diff", "repository": "https://github.com/HolodoriDB/holodori-db-eng-diff",
"commit": "f7428d4092d57b34311f030e00ff46e7dbbc29e8", "commit": "7f69b2404abe3001ce411240ed2323a6313a2708",
"commitDate": "2026-09-18T10:50:49Z", "commitDate": "2026-09-26T16:03:09Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
}, },
"kor": { "kor": {
"repository": "https://github.com/HolodoriDB/holodori-db-kor-diff", "repository": "https://github.com/HolodoriDB/holodori-db-kor-diff",
"commit": "5517e40b931fec10253f5b850989346518e233c2", "commit": "98f8b557a095c837b51126d993c34d582fb5c5f2",
"commitDate": "2026-09-18T13:17:53Z", "commitDate": "2026-09-26T13:30:40Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
}, },
"jpn": { "jpn": {
"repository": "https://github.com/HolodoriDB/holodori-db-jpn-diff", "repository": "https://github.com/HolodoriDB/holodori-db-jpn-diff",
"commit": "a22c8e29a981a8987bf2074c245e2fc40ad4dbea", "commit": "e43f062c32ff4e04567235efcd58448cd6b10f35",
"commitDate": "2026-09-18T11:03:55Z", "commitDate": "2026-09-26T16:18:53Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
} }
}, },
"cards": [ "cards": [
+10 -10
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-09-19T01:15:22.929Z", "generatedAt": "2026-09-26T21:34:40.634Z",
"source": "HolodoriDB community master-data diff repositories", "source": "HolodoriDB community master-data diff repositories",
"sourceLicense": null, "sourceLicense": null,
"sourceLicenseNote": "No LICENSE file was present in the three source repositories at collection time.", "sourceLicenseNote": "No LICENSE file was present in the three source repositories at collection time.",
@@ -8,21 +8,21 @@
"sources": { "sources": {
"eng": { "eng": {
"repository": "https://github.com/HolodoriDB/holodori-db-eng-diff", "repository": "https://github.com/HolodoriDB/holodori-db-eng-diff",
"commit": "f7428d4092d57b34311f030e00ff46e7dbbc29e8", "commit": "7f69b2404abe3001ce411240ed2323a6313a2708",
"commitDate": "2026-09-18T10:50:49Z", "commitDate": "2026-09-26T16:03:09Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
}, },
"kor": { "kor": {
"repository": "https://github.com/HolodoriDB/holodori-db-kor-diff", "repository": "https://github.com/HolodoriDB/holodori-db-kor-diff",
"commit": "5517e40b931fec10253f5b850989346518e233c2", "commit": "98f8b557a095c837b51126d993c34d582fb5c5f2",
"commitDate": "2026-09-18T13:17:53Z", "commitDate": "2026-09-26T13:30:40Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
}, },
"jpn": { "jpn": {
"repository": "https://github.com/HolodoriDB/holodori-db-jpn-diff", "repository": "https://github.com/HolodoriDB/holodori-db-jpn-diff",
"commit": "a22c8e29a981a8987bf2074c245e2fc40ad4dbea", "commit": "e43f062c32ff4e04567235efcd58448cd6b10f35",
"commitDate": "2026-09-18T11:03:55Z", "commitDate": "2026-09-26T16:18:53Z",
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9" "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd"
} }
}, },
"counts": { "counts": {
+11
View File
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"songs": {
"m0525": {
"page": "https://holodori.best/songs/m0525",
"acquisition": "User-provided song detail JSON",
"file": "docs/research/m0525-song-detail.json",
"sha256": "3d532b5e5fec73a145a04efd9d17d63d0ddf537b5dac015233a4778bf3cd5198"
}
}
}
+149
View File
@@ -0,0 +1,149 @@
{
"source": {
"repository": "https://github.com/HolodoriDB/holodori-db-kor-diff.git",
"commit": "98f8b557a095c837b51126d993c34d582fb5c5f2",
"locale": "kor"
},
"music": {
"id": "m0525",
"titleLangId": "la-music_title-m0525",
"titleRubyLangId": "la-music_title_ruby-m0525",
"lyricistLangId": "la-music_lyricist-m0525",
"lyricistRubyLangId": "la-music_lyricist_ruby-m0525",
"composerLangId": "la-music_composer-m0525",
"composerRubyLangId": "la-music_composer_ruby-m0525",
"arrangerLangId": "la-music_arranger-m0525",
"arrangerRubyLangId": "la-music_arranger_ruby-m0525",
"jacketAssetId": "m0525",
"startTime": "1790424000000",
"playingSeconds": 122,
"categoryType": "MusicCategoryType_MUSIC_CATEGORY_TYPE_ORIGINAL",
"liveScoreCoefficientPermil": 5,
"singleLiveScoreEvaluationRankGroupId": "live_score_rank-s001",
"multiLiveScoreEvaluationRankGroupId": "live_score_rank-m001",
"liveComboGroupId": "live_combo-1",
"releaseType": "MusicReleaseType_MUSIC_RELEASE_TYPE_PRESET",
"isHighestScoreRatingTarget": true,
"mvUrl": "https://youtu.be/iSQYbf9XFg8",
"mvThumbnailAssetId": "m0525-mv-01",
"musicSingerType": "MusicSingerType_MUSIC_SINGER_TYPE_GROUP",
"characterIds": [
"chr-00030",
"chr-00031",
"chr-00032",
"chr-00034",
"chr-00035",
"chr-00036",
"chr-00037",
"chr-00039"
],
"characterGroupDisplayNameLangId": "la-singer_name-m0525",
"characterGroupDisplayNameRubyLangId": "la-singer_name_ruby-m0525",
"chorusStartMillisecond": 68571,
"chorusEndMillisecond": 98285,
"assetId": "m0525",
"order": 40000525
},
"difficulties": [
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_EASY",
"difficultyLevel": 6,
"chartAssetId": "chart_m0525_easy"
},
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_EXPERT",
"difficultyLevel": 27,
"chartAssetId": "chart_m0525_expert"
},
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_HARD",
"difficultyLevel": 22,
"chartAssetId": "chart_m0525_hard"
},
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_NORMAL",
"difficultyLevel": 13,
"chartAssetId": "chart_m0525_normal"
}
],
"charts": [
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_EASY",
"fullComboNoteCount": 280,
"maxComboCountRewardThreshold": 200,
"chartHash": "d57f91ec1c09e69aaf2018f794924b9f",
"normalNoteCount": 82
},
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_EXPERT",
"fullComboNoteCount": 1144,
"maxComboCountRewardThreshold": 900,
"chartHash": "8ad3680f19803518e6a9d50a6bc27d6a",
"normalNoteCount": 352
},
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_HARD",
"fullComboNoteCount": 812,
"maxComboCountRewardThreshold": 650,
"chartHash": "773b12269be49f29b209fe541ac06744",
"normalNoteCount": 337
},
{
"musicId": "m0525",
"difficultyType": "MusicDifficultyType_MUSIC_DIFFICULTY_TYPE_NORMAL",
"fullComboNoteCount": 432,
"maxComboCountRewardThreshold": 350,
"chartHash": "17aa683df422800347a952dd579ba557",
"normalNoteCount": 200
}
],
"strings": [
{
"id": "la-music_arranger-m0525",
"text": "PandaBoY"
},
{
"id": "la-music_arranger_ruby-m0525",
"text": "PandaBoY"
},
{
"id": "la-music_composer-m0525",
"text": "PandaBoY"
},
{
"id": "la-music_composer_ruby-m0525",
"text": "PandaBoY"
},
{
"id": "la-music_lyricist-m0525",
"text": "PandaBoY"
},
{
"id": "la-music_lyricist_ruby-m0525",
"text": "PandaBoY"
},
{
"id": "la-music_title-m0525",
"text": "Watcha Gatcha!!!!!!!!"
},
{
"id": "la-music_title_ruby-m0525",
"text": "WatchaGatcha!!!!!!!!"
},
{
"id": "la-singer_name-m0525",
"text": "hololive 5th Generation, Secret Society holoX"
},
{
"id": "la-singer_name_ruby-m0525",
"text": "hololive5thGenerationSecretSocietyholoX"
}
]
}
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -286,6 +286,10 @@
{"songId":"m0352","difficulty":"easy","susSha256":"d31a500a280011ad6da7d48ef23b950ef59fe649e1135408166277f76d35223c","fullComboNoteCount":220,"counts":{"NORMAL":64,"FLICK":0,"LONG_START":21,"LONG_END":21,"LONG_FLICK_END":0,"LONG_RELAY":3,"LONG_CONTINUE":111},"invisibleRelayCount":0,"bezierPointCount":0,"baseWeight":117400,"perfectNoteWeight":117400,"liveScoreCoefficientPermil":7,"difficultyLevel":3,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"}, {"songId":"m0352","difficulty":"easy","susSha256":"d31a500a280011ad6da7d48ef23b950ef59fe649e1135408166277f76d35223c","fullComboNoteCount":220,"counts":{"NORMAL":64,"FLICK":0,"LONG_START":21,"LONG_END":21,"LONG_FLICK_END":0,"LONG_RELAY":3,"LONG_CONTINUE":111},"invisibleRelayCount":0,"bezierPointCount":0,"baseWeight":117400,"perfectNoteWeight":117400,"liveScoreCoefficientPermil":7,"difficultyLevel":3,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"},
{"songId":"m0352","difficulty":"normal","susSha256":"097fddec319e17d1df3669068e794f826ea24a95b1b1f3a3959211570a309dd9","fullComboNoteCount":364,"counts":{"NORMAL":186,"FLICK":0,"LONG_START":32,"LONG_END":32,"LONG_FLICK_END":0,"LONG_RELAY":13,"LONG_CONTINUE":101},"invisibleRelayCount":0,"bezierPointCount":0,"baseWeight":261400,"perfectNoteWeight":261400,"liveScoreCoefficientPermil":7,"difficultyLevel":11,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"}, {"songId":"m0352","difficulty":"normal","susSha256":"097fddec319e17d1df3669068e794f826ea24a95b1b1f3a3959211570a309dd9","fullComboNoteCount":364,"counts":{"NORMAL":186,"FLICK":0,"LONG_START":32,"LONG_END":32,"LONG_FLICK_END":0,"LONG_RELAY":13,"LONG_CONTINUE":101},"invisibleRelayCount":0,"bezierPointCount":0,"baseWeight":261400,"perfectNoteWeight":261400,"liveScoreCoefficientPermil":7,"difficultyLevel":11,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"},
{"songId":"m0352","difficulty":"hard","susSha256":"f39acc366a08d344740cdad5d5dc80551d1df9f9dcaedb862558677c0e58ce5d","fullComboNoteCount":640,"counts":{"NORMAL":321,"FLICK":30,"LONG_START":74,"LONG_END":63,"LONG_FLICK_END":11,"LONG_RELAY":25,"LONG_CONTINUE":116},"invisibleRelayCount":15,"bezierPointCount":0,"baseWeight":514600,"perfectNoteWeight":516100,"liveScoreCoefficientPermil":7,"difficultyLevel":19,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"}, {"songId":"m0352","difficulty":"hard","susSha256":"f39acc366a08d344740cdad5d5dc80551d1df9f9dcaedb862558677c0e58ce5d","fullComboNoteCount":640,"counts":{"NORMAL":321,"FLICK":30,"LONG_START":74,"LONG_END":63,"LONG_FLICK_END":11,"LONG_RELAY":25,"LONG_CONTINUE":116},"invisibleRelayCount":15,"bezierPointCount":0,"baseWeight":514600,"perfectNoteWeight":516100,"liveScoreCoefficientPermil":7,"difficultyLevel":19,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"},
{"songId":"m0352","difficulty":"expert","susSha256":"71b330468b4d87a0db9f5734884934cf17252ed90ed69c8440b88122d550b0cb","fullComboNoteCount":1064,"counts":{"NORMAL":558,"FLICK":33,"LONG_START":135,"LONG_END":104,"LONG_FLICK_END":31,"LONG_RELAY":84,"LONG_CONTINUE":119},"invisibleRelayCount":227,"bezierPointCount":0,"baseWeight":882950,"perfectNoteWeight":905650,"liveScoreCoefficientPermil":7,"difficultyLevel":26,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"} {"songId":"m0352","difficulty":"expert","susSha256":"71b330468b4d87a0db9f5734884934cf17252ed90ed69c8440b88122d550b0cb","fullComboNoteCount":1064,"counts":{"NORMAL":558,"FLICK":33,"LONG_START":135,"LONG_END":104,"LONG_FLICK_END":31,"LONG_RELAY":84,"LONG_CONTINUE":119},"invisibleRelayCount":227,"bezierPointCount":0,"baseWeight":882950,"perfectNoteWeight":905650,"liveScoreCoefficientPermil":7,"difficultyLevel":26,"catalogRevision":84,"masterCommit":"5517e40b931fec10253f5b850989346518e233c2"},
{"songId": "m0525", "fullComboNoteCount": 280, "counts": {"NORMAL": 82, "FLICK": 0, "LONG_START": 33, "LONG_END": 33, "LONG_FLICK_END": 0, "LONG_RELAY": 0, "LONG_CONTINUE": 132}, "invisibleRelayCount": 0, "bezierPointCount": 0, "baseWeight": 161200, "perfectNoteWeight": 161200, "difficulty": "easy", "susSha256": "370f884efeb1a407792fb5ea56be1a7ef43a0aeba590002e85fc50dcc9341fbd", "liveScoreCoefficientPermil": 5, "difficultyLevel": 6, "catalogRevision": 86, "chartHash": "d57f91ec1c09e69aaf2018f794924b9f", "metadataSource": {"page": "https://holodori.best/songs/m0525", "acquisition": "User-provided song detail JSON", "file": "docs/research/m0525-song-detail.json", "sha256": "3d532b5e5fec73a145a04efd9d17d63d0ddf537b5dac015233a4778bf3cd5198"}, "masterCommit": "98f8b557a095c837b51126d993c34d582fb5c5f2"},
{"songId": "m0525", "fullComboNoteCount": 432, "counts": {"NORMAL": 200, "FLICK": 0, "LONG_START": 40, "LONG_END": 40, "LONG_FLICK_END": 0, "LONG_RELAY": 4, "LONG_CONTINUE": 148}, "invisibleRelayCount": 7, "bezierPointCount": 0, "baseWeight": 295200, "perfectNoteWeight": 295900, "difficulty": "normal", "susSha256": "1bf7c2eb76b7e517352156a0d07a0e2e076e7c44f36efd8a1457488a06661b4a", "liveScoreCoefficientPermil": 5, "difficultyLevel": 13, "catalogRevision": 86, "chartHash": "17aa683df422800347a952dd579ba557", "metadataSource": {"page": "https://holodori.best/songs/m0525", "acquisition": "User-provided song detail JSON", "file": "docs/research/m0525-song-detail.json", "sha256": "3d532b5e5fec73a145a04efd9d17d63d0ddf537b5dac015233a4778bf3cd5198"}, "masterCommit": "98f8b557a095c837b51126d993c34d582fb5c5f2"},
{"songId": "m0525", "fullComboNoteCount": 812, "counts": {"NORMAL": 337, "FLICK": 26, "LONG_START": 95, "LONG_END": 81, "LONG_FLICK_END": 14, "LONG_RELAY": 50, "LONG_CONTINUE": 209}, "invisibleRelayCount": 174, "bezierPointCount": 0, "baseWeight": 580200, "perfectNoteWeight": 597600, "difficulty": "hard", "susSha256": "36a6a60665282ff3971a628d71e98f7b312cd3cef953dd4fd93a08d607386e42", "liveScoreCoefficientPermil": 5, "difficultyLevel": 22, "catalogRevision": 86, "chartHash": "773b12269be49f29b209fe541ac06744", "metadataSource": {"page": "https://holodori.best/songs/m0525", "acquisition": "User-provided song detail JSON", "file": "docs/research/m0525-song-detail.json", "sha256": "3d532b5e5fec73a145a04efd9d17d63d0ddf537b5dac015233a4778bf3cd5198"}, "masterCommit": "98f8b557a095c837b51126d993c34d582fb5c5f2"},
{"songId": "m0525", "fullComboNoteCount": 1144, "counts": {"NORMAL": 352, "FLICK": 49, "LONG_START": 208, "LONG_END": 187, "LONG_FLICK_END": 21, "LONG_RELAY": 62, "LONG_CONTINUE": 265}, "invisibleRelayCount": 203, "bezierPointCount": 0, "baseWeight": 852150, "perfectNoteWeight": 872450, "difficulty": "expert", "susSha256": "ffca5baeeab5e5dd32449fb95a8f40bcb045cc8c8ce35920c48677e69ef2b523", "liveScoreCoefficientPermil": 5, "difficultyLevel": 27, "catalogRevision": 86, "chartHash": "8ad3680f19803518e6a9d50a6bc27d6a", "metadataSource": {"page": "https://holodori.best/songs/m0525", "acquisition": "User-provided song detail JSON", "file": "docs/research/m0525-song-detail.json", "sha256": "3d532b5e5fec73a145a04efd9d17d63d0ddf537b5dac015233a4778bf3cd5198"}, "masterCommit": "98f8b557a095c837b51126d993c34d582fb5c5f2"}
] ]
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"repository": "https://github.com/DD4SN/DD4SN.github.io.git", "repository": "https://github.com/DD4SN/DD4SN.github.io.git",
"approvedMasterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9", "approvedMasterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd",
"approvedNotBefore": "2026-09-19T01:00:00.000Z" "approvedNotBefore": "2026-09-26T12:00:00.000Z"
} }
+33 -6
View File
@@ -1,17 +1,20 @@
// Convert Hololive Dreams' decrypted SUS dialect to this application's compact score timeline. // Convert Hololive Dreams' decrypted SUS dialect to this application's compact score timeline.
import { readFile, writeFile } from 'node:fs/promises'; import { readFile, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { readHolodoriDbTable as readMaster } from './holodoridb-source.mjs'; import { readHolodoriDbTable as readMaster, prepareHolodoriDbSource } from './holodoridb-source.mjs';
import path from 'node:path'; import path from 'node:path';
import { susScoreWeight } from './sus-score-weight.mjs'; import { susScoreWeight } from './sus-score-weight.mjs';
import { formulaScoreMetadata } from './song-score-formulas.mjs'; import { formulaScoreMetadata } from './song-score-formulas.mjs';
import { parseSusTimeline } from './sus-timeline.mjs'; import { parseSusTimeline } from './sus-timeline.mjs';
const difficulties = ['easy', 'normal', 'hard', 'expert']; const difficulties = ['easy', 'normal', 'hard', 'expert'];
const overrides = JSON.parse(await readFile(new URL('../data/song-metadata-overrides.json', import.meta.url), 'utf8')).songs;
const music = new Map(readMaster('kor', 'Music.json').map((row) => { const data = row.data ?? row; return [data.id, data]; })); const music = new Map(readMaster('kor', 'Music.json').map((row) => { const data = row.data ?? row; return [data.id, data]; }));
const translation = new Map(readMaster('kor', 'LangMusic_Kor.json').map((row) => { const data = row.data ?? row; return [data.id, data.text]; })); const translation = new Map(readMaster('kor', 'LangMusic_Kor.json').map((row) => { const data = row.data ?? row; return [data.id, data.text]; }));
const levels = new Map(readMaster('kor', 'MusicDifficulty.json').map((row) => { const data = row.data ?? row; return [`${data.musicId}.${data.difficultyType.split('_').at(-1).toLowerCase()}`, data.difficultyLevel]; })); const levels = new Map(readMaster('kor', 'MusicDifficulty.json').map((row) => { const data = row.data ?? row; return [`${data.musicId}.${data.difficultyType.split('_').at(-1).toLowerCase()}`, data.difficultyLevel]; }));
const reviewedSource = prepareHolodoriDbSource('kor');
const masterDataSource = { repository: reviewedSource.repository, commit: reviewedSource.commit, locale: 'kor' };
const songIds = process.argv.slice(2); const songIds = process.argv.slice(2);
if (songIds.length === 0) throw new Error('곡 ID 또는 곡 ID.난이도를 지정하세요.'); if (songIds.length === 0) throw new Error('곡 ID 또는 곡 ID.난이도를 지정하세요.');
const selected = songIds.flatMap((value) => { const selected = songIds.flatMap((value) => {
@@ -30,13 +33,37 @@ for (const { songId, difficulty } of selected) {
const mids = timeline.notes.filter((note) => note.type === 'mid').length; const mids = timeline.notes.filter((note) => note.type === 'mid').length;
throw new Error(`${songId}.${difficulty}: 파서 노트 수 ${timeline.notes.length}/${weight.fullComboNoteCount} 불일치 (중간 ${mids}, 헤더 ${weight.counts.LONG_RELAY + weight.counts.LONG_CONTINUE})`); throw new Error(`${songId}.${difficulty}: 파서 노트 수 ${timeline.notes.length}/${weight.fullComboNoteCount} 불일치 (중간 ${mids}, 헤더 ${weight.counts.LONG_RELAY + weight.counts.LONG_CONTINUE})`);
} }
const master = music.get(songId); const officialMaster = music.get(songId);
const difficultyLevel = levels.get(`${songId}.${difficulty}`); const metadataSource = !officialMaster ? overrides[songId] : undefined;
let supplemental;
if (metadataSource) {
const metadataBytes = await readFile(path.resolve(metadataSource.file));
if (createHash('sha256').update(metadataBytes).digest('hex') !== metadataSource.sha256) {
throw new Error(songId + ': 보충 메타데이터 해시가 다릅니다.');
}
supplemental = JSON.parse(metadataBytes.toString('utf8'));
if (supplemental.id !== songId) throw new Error(songId + ': 보충 메타데이터 곡 ID가 다릅니다.');
}
// Fresh official master takes precedence over the pinned supplementary response.
const master = officialMaster ?? supplemental;
const supplementalDifficulty = supplemental?.difficulties.find((row) => row.difficultyType.split('_').at(-1).toLowerCase() === difficulty);
const difficultyLevel = levels.get(songId + '.' + difficulty) ?? supplementalDifficulty?.difficultyLevel;
const releaseTime = Number(master?.startTime);
const durationSeconds = master?.playingSeconds;
if (!master || !Number.isInteger(difficultyLevel) || difficultyLevel < 1 || !Number.isFinite(releaseTime) ||
!Number.isFinite(durationSeconds) || durationSeconds < timeline.notes.at(-1).timeSeconds ||
!Number.isFinite(master.liveScoreCoefficientPermil) || !Array.isArray(master.characterIds) || !master.musicSingerType) {
throw new Error(songId + '.' + difficulty + ': 검증된 곡 메타데이터가 필요합니다.');
}
if (supplementalDifficulty && (supplementalDifficulty.fullComboNoteCount !== weight.fullComboNoteCount || supplementalDifficulty.normalNoteCount !== weight.counts.NORMAL)) {
throw new Error(songId + '.' + difficulty + ': 메타데이터와 SUS 노트 수가 다릅니다.');
}
const susSha256 = createHash('sha256').update(bytes).digest('hex'); const susSha256 = createHash('sha256').update(bytes).digest('hex');
const formula = formulaScoreMetadata(songId, difficulty, difficultyLevel, timeline.notes.length, master.liveScoreCoefficientPermil, susSha256); const formula = formulaScoreMetadata(songId, difficulty, difficultyLevel, timeline.notes.length, master.liveScoreCoefficientPermil, susSha256);
const output = { schemaVersion: 1, songId, difficulty, durationSeconds: master.playingSeconds, scoreRatio: formula?.scoreRatio ?? null, scoreCalibrationSource: formula?.scoreCalibrationSource ?? null, ...(formula ?? {}), notes: timeline.notes, specialStartSeconds: timeline.specialStartSeconds, singerType: master.musicSingerType?.split('_').at(-1).toLowerCase() ?? null, singerCharacterIds: master.characterIds ?? [], source: { page: 'Hololive Dreams CDN via holodori-asset-tools', transcriptionStatus: 'exact_from_decrypted_sus', susSha256 } }; if (!formula) throw new Error(songId + '.' + difficulty + ': 검증된 점수 공식이 필요합니다.');
outputs.push({ filename: path.resolve(`static/data/charts/${songId}.${difficulty}.json`), output }); const output = { schemaVersion: 1, songId, difficulty, durationSeconds, ...formula, notes: timeline.notes, specialStartSeconds: timeline.specialStartSeconds, singerType: master.musicSingerType.split('_').at(-1).toLowerCase(), singerCharacterIds: master.characterIds, source: { page: 'Hololive Dreams CDN via holodori-asset-tools', transcriptionStatus: 'exact_from_decrypted_sus', susSha256, ...(metadataSource ? { metadataSource } : { masterDataSource }) } };
generatedCatalogEntries.push({ songId, title: translation.get(master.titleLangId) ?? songId, difficulty, difficultyLevel, durationSeconds: master.playingSeconds, scoreRatio: output.scoreRatio, scoreCalibrationSource: output.scoreCalibrationSource, scoreFormula: output.scoreFormula, totalNotes: output.notes.length, singerType: output.singerType, singerCharacterIds: output.singerCharacterIds, releaseTime: Number(master.startTime), path: `/data/charts/${songId}.${difficulty}.json` }); outputs.push({ filename: path.resolve('static/data/charts/' + songId + '.' + difficulty + '.json'), output });
generatedCatalogEntries.push({ songId, title: translation.get(master.titleLangId) ?? supplemental?.title?.kor ?? songId, difficulty, difficultyLevel, durationSeconds, scoreRatio: output.scoreRatio, scoreCalibrationSource: output.scoreCalibrationSource, scoreFormula: output.scoreFormula, totalNotes: output.notes.length, singerType: output.singerType, singerCharacterIds: output.singerCharacterIds, releaseTime, path: '/data/charts/' + songId + '.' + difficulty + '.json' });
console.log(`${songId}.${difficulty}: ${timeline.notes.length} notes ${timeline.specialStartSeconds.join(', ')}`); console.log(`${songId}.${difficulty}: ${timeline.notes.length} notes ${timeline.specialStartSeconds.join(', ')}`);
} }
const catalogPath = path.resolve('src/lib/data/exact-chart-catalog.json'); const catalogPath = path.resolve('src/lib/data/exact-chart-catalog.json');
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9", "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd",
"members": [ "members": [
{ {
"id": "chr-00001", "id": "chr-00001",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9", "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd",
"cards": [ "cards": [
{ {
"id": "card-00001-5-uniq-0000-00", "id": "card-00001-5-uniq-0000-00",
+112
View File
@@ -17122,6 +17122,118 @@
"difficultyLevel": 10 "difficultyLevel": 10
} }
}, },
{
"songId": "m0525",
"title": "Watcha Gatcha!!!!!!!!",
"difficulty": "easy",
"difficultyLevel": 6,
"durationSeconds": 122,
"scoreRatio": 69.73826519576033,
"scoreCalibrationSource": "formula-v1",
"scoreFormula": {
"perfectNoteWeight": 161200,
"liveScoreCoefficientPermil": 5,
"difficultyLevel": 6
},
"totalNotes": 280,
"singerType": "group",
"singerCharacterIds": [
"chr-00030",
"chr-00031",
"chr-00032",
"chr-00034",
"chr-00035",
"chr-00036",
"chr-00037",
"chr-00039"
],
"releaseTime": 1790424000000,
"path": "/data/charts/m0525.easy.json"
},
{
"songId": "m0525",
"title": "Watcha Gatcha!!!!!!!!",
"difficulty": "expert",
"difficultyLevel": 27,
"durationSeconds": 122,
"scoreRatio": 341.73521347434394,
"scoreCalibrationSource": "formula-v1",
"scoreFormula": {
"perfectNoteWeight": 872450,
"liveScoreCoefficientPermil": 5,
"difficultyLevel": 27
},
"totalNotes": 1144,
"singerType": "group",
"singerCharacterIds": [
"chr-00030",
"chr-00031",
"chr-00032",
"chr-00034",
"chr-00035",
"chr-00036",
"chr-00037",
"chr-00039"
],
"releaseTime": 1790424000000,
"path": "/data/charts/m0525.expert.json"
},
{
"songId": "m0525",
"title": "Watcha Gatcha!!!!!!!!",
"difficulty": "hard",
"difficultyLevel": 22,
"durationSeconds": 122,
"scoreRatio": 239.47104788619518,
"scoreCalibrationSource": "formula-v1",
"scoreFormula": {
"perfectNoteWeight": 597600,
"liveScoreCoefficientPermil": 5,
"difficultyLevel": 22
},
"totalNotes": 812,
"singerType": "group",
"singerCharacterIds": [
"chr-00030",
"chr-00031",
"chr-00032",
"chr-00034",
"chr-00035",
"chr-00036",
"chr-00037",
"chr-00039"
],
"releaseTime": 1790424000000,
"path": "/data/charts/m0525.hard.json"
},
{
"songId": "m0525",
"title": "Watcha Gatcha!!!!!!!!",
"difficulty": "normal",
"difficultyLevel": 13,
"durationSeconds": 122,
"scoreRatio": 123.70401337792642,
"scoreCalibrationSource": "formula-v1",
"scoreFormula": {
"perfectNoteWeight": 295900,
"liveScoreCoefficientPermil": 5,
"difficultyLevel": 13
},
"totalNotes": 432,
"singerType": "group",
"singerCharacterIds": [
"chr-00030",
"chr-00031",
"chr-00032",
"chr-00034",
"chr-00035",
"chr-00036",
"chr-00037",
"chr-00039"
],
"releaseTime": 1790424000000,
"path": "/data/charts/m0525.normal.json"
},
{ {
"songId": "m0527", "songId": "m0527",
"title": "Mekurumeku Rendezvous", "title": "Mekurumeku Rendezvous",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"masterDataVersion": "927a8489062cfb8aaee197689930a4c4e9c622e9ebbf8b80577bdbe5f065dcf9", "masterDataVersion": "845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd",
"maxCount": 34, "maxCount": 34,
"effects": [ "effects": [
{ {
+44
View File
@@ -1,3 +1,7 @@
import { createHash } from 'node:crypto';
import { leaderRatingCharts } from './song-rating.ts';
import overrides from '../../../data/song-metadata-overrides.json' with { type: 'json' };
import officialSong from '../../../docs/research/m0525-holodoridb.json' with { type: 'json' };
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
@@ -62,3 +66,43 @@ test('잘못된 분모와 난이도로 공식 환산값을 만들지 않는다',
assert.throws(() => scoreRatioFromFormula({ ...valid, ...invalid })); assert.throws(() => scoreRatioFromFormula({ ...valid, ...invalid }));
} }
}); });
test('m0525는 보충 응답과 일치하는 공식 마스터를 우선 적용한다', async () => {
const source = overrides.songs.m0525;
const bytes = await readFile(new URL(`../../../${source.file}`, import.meta.url));
assert.equal(createHash('sha256').update(bytes).digest('hex'), source.sha256);
const master = JSON.parse(bytes.toString('utf8'));
const charts = catalog.charts.filter((row) => row.songId === master.id);
assert.equal(charts.length, 4);
const releaseTime = Number(master.startTime);
const weights = { easy: 161200, normal: 295900, hard: 597600, expert: 872450 };
for (const row of charts) {
const difficulty = master.difficulties.find((item: { difficultyType: string }) => item.difficultyType.toLowerCase().endsWith('_' + row.difficulty));
const chart = JSON.parse(await readFile(new URL(`../../../static${row.path}`, import.meta.url), 'utf8'));
const officialDifficulty = officialSong.difficulties.find((item) => item.difficultyType === difficulty.difficultyType);
const officialChart = officialSong.charts.find((item) => item.difficultyType === difficulty.difficultyType);
assert.equal(row.difficultyLevel, officialDifficulty?.difficultyLevel);
assert.equal(row.totalNotes, officialChart?.fullComboNoteCount);
assert.equal(officialChart?.chartHash, difficulty.chartHash);
assert.equal(row.difficultyLevel, difficulty.difficultyLevel);
assert.equal(row.totalNotes, difficulty.fullComboNoteCount);
assert.equal(row.title, master.title.kor);
assert.equal(row.durationSeconds, master.playingSeconds);
assert.equal(row.releaseTime, releaseTime);
assert.equal(row.scoreFormula.liveScoreCoefficientPermil, master.liveScoreCoefficientPermil);
assert.equal(row.scoreFormula.perfectNoteWeight, weights[row.difficulty as keyof typeof weights]);
assert.equal(row.singerType, 'group');
assert.deepEqual(row.singerCharacterIds, master.characterIds);
assert.deepEqual(chart.singerCharacterIds, master.characterIds);
assert.deepEqual(chart.source.masterDataSource, officialSong.source);
assert.equal(chart.source.metadataSource, undefined);
assert.deepEqual(chart.singerCharacterIds, officialSong.music.characterIds);
assert.equal(row.scoreFormula.liveScoreCoefficientPermil, officialSong.music.liveScoreCoefficientPermil);
assert.equal(chart.specialStartSeconds.length, 5);
assert.ok(chart.notes.every((note: { timeSeconds: number }) => Number.isFinite(note.timeSeconds) && note.timeSeconds <= row.durationSeconds));
}
assert.equal(master.characterIds.length, 8);
assert.equal(leaderRatingCharts(charts, 'chr-00030', 'all', releaseTime - 1).length, 0);
assert.equal(leaderRatingCharts(charts, 'chr-00030', 'all', releaseTime).length, 4);
assert.equal(leaderRatingCharts(charts, 'chr-00038', 'all', releaseTime).length, 0);
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long