1/** 2 * --- 3 * Project : Coding Campus Platform 4 * Module : Static / Js 5 * File : keyboard_beat.js 6 * 7 * Description : Keyboard Beat â PLAY LAB ë¦¬ë¬ íì MVP 8 * 9 * Author : Jihun Kang 10 * Organization : Coding Campus / OrbiSync 11 * Contact : [email protected] 12 * 13 * Copyright (c) 2026 Jihun Kang. All rights reserved. 14 * --- 15 */ 16/** 17 * Keyboard Beat â PLAY LAB ë¦¬ë¬ íì MVP 18 * Web Audio APIë§ ì¬ì© (ì¸ë¶ ìì ìì). 19 */ 20(function () { 21 "use strict"; 22 23 function updateAppHeight() { 24 try { 25 var h = window.innerHeight; 26 if (window.visualViewport && window.visualViewport.height > 0) { 27 h = Math.round(window.visualViewport.height); 28 } 29 document.documentElement.style.setProperty("--app-height", h + "px"); 30 } catch (eAppH) { 31 /* ignore */ 32 } 33 } 34 35 function onBeatViewportChange() { 36 updateAppHeight(); 37 if (state.phase === "playing") { 38 syncBeatPlayfieldLayout(true, "viewportChange"); 39 } 40 } 41 42 function beatQuery(selector, root) { 43 var scope = root || document; 44 try { 45 return scope.querySelector(selector); 46 } catch (eQ) { 47 return null; 48 } 49 } 50 51 function setTextById(id, value) { 52 var el = document.getElementById(id); 53 if (!el) { 54 console.warn("[Keyboard Beat] missing element:", id); 55 return; 56 } 57 el.textContent = value != null ? String(value) : ""; 58 } 59 60 function bindClick(el, handler, label) { 61 if (!el || typeof handler !== "function") { 62 if (label) console.warn("[Keyboard Beat] missing element for click:", label); 63 return; 64 } 65 el.addEventListener("click", handler); 66 } 67 68 function logBeatDomSnapshot(phase) { 69 if (typeof console === "undefined" || !console.debug) return; 70 console.debug("[Keyboard Beat] " + phase, { 71 panelActive: isBeatPanelActive(), 72 intro: !!beatQuery("#kbSetup:not(.is-hidden)"), 73 board: !!beatQuery(".kb-board"), 74 stage: !!beatQuery(".kb-lane__stage"), 75 startButton: !!beatQuery("#kbStartBtn"), 76 keypad: !!beatQuery("#kbTouchRow"), 77 gameVisible: !!beatQuery("#kbGame:not(.is-hidden)"), 78 }); 79 } 80 81 function isBeatPanelActive() { 82 var panel = document.getElementById("playLabPanelKeyboardBeat"); 83 return !!(panel && panel.classList.contains("is-active")); 84 } 85 86 function parkBeatPanelUi() { 87 setBeatPlayingLayout(false); 88 beatChrome.teardown(); 89 closeKbRankingLayer(); 90 closeKbSettingsLayer(); 91 closeKbBeatPreviewModal(); 92 if (els.setup) { 93 els.setup.classList.add("is-hidden"); 94 els.setup.setAttribute("aria-hidden", "true"); 95 } 96 if (els.game) els.game.classList.add("is-hidden"); 97 if (els.countdown) els.countdown.classList.add("is-hidden"); 98 if (els.resultOverlay) els.resultOverlay.classList.add("is-hidden"); 99 if (els.touchRow) { 100 els.touchRow.classList.add("is-hidden"); 101 els.touchRow.innerHTML = ""; 102 } 103 } 104 105 /* -------------------------------------------------------------------------- */ 106 /* Key sets & difficulty (BPM) */ 107 /* -------------------------------------------------------------------------- */ 108 109 var KEY_SETS = { 110 home: ["A", "S", "D", "F", "J", "K", "L", ";"], 111 left: ["Q", "W", "E", "R", "T", "A", "S", "D", "F", "G", "Z", "X", "C", "V", "B"], 112 right: ["Y", "U", "I", "O", "P", "H", "J", "K", "L", "N", "M"], 113 numbers: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], 114 symbols: ["(", ")", "{", "}", "[", "]", ";", '"', "'", "=", "+", "-", "*", "/", "<", ">"], 115 }; 116 117 var BPM_BY_DIFF = { easy: 90, normal: 110, hard: 130 }; 118 119 var LEVEL_DURATION = 30; 120 var COUNTDOWN_SEC = 3; 121 var TRAVEL_MS = 1600; 122 var FALL_SPEED_TRAVEL_NUMERATOR = 168000; 123 124 var BEAT_DEBUG = false; 125 try { 126 BEAT_DEBUG = 127 /(?:\?|&)kb_debug=1(?:&|$)/.test(window.location.search) || 128 window.localStorage.getItem("keyboard_beat_debug") === "1"; 129 } catch (eDbg) { 130 BEAT_DEBUG = false; 131 } 132 133 function beatStringifySafe(value) { 134 if (value == null || typeof value !== "object") return String(value); 135 try { 136 return JSON.stringify(value); 137 } catch (eStr) { 138 return String(value); 139 } 140 } 141 142 function beatDebugLog() { 143 if (!BEAT_DEBUG || typeof console === "undefined" || !console.log) return; 144 var parts = ["[Keyboard Beat debug]"]; 145 for (var i = 0; i < arguments.length; i++) { 146 parts.push(beatStringifySafe(arguments[i])); 147 } 148 console.log(parts.join(" ")); 149 } 150 151 /** ë ë²¨ë³ í¤ í â ë¨ê³ì íì¥ (resolveBeatKeysForLevelìì ì ì©) */ 152 var BEAT_KEY_PROGRESSION = [ 153 ["A", "S", "D", "F"], 154 ["A", "S", "D", "F", "J", "K", "L"], 155 ["A", "S", "D", "F", "G", "H", "J", "K", "L"], 156 ["A", "S", "D", "F", "G", "H", "J", "K", "L", ";"], 157 ["A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "Q", "W", "E", "R", "T", "Y"], 158 ["A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], 159 [ 160 "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "Z", 161 "X", "C", "V", "B", "N", "M", 162 ], 163 ]; 164 165 /**
166 * ë ë²¨ë³ ìë·ì¤í°Â·ì íë (minSpeed/maxSpeed = ëí ë°°ì¨, baseFall = ê¸°ì¤ fallSpeed) 167 */ 168 var BEAT_LEVELS = [ 169 { level: 1, baseFall: 68, minSpeed: 1.4, maxSpeed: 1.8, spawnInterval: 1650, requiredAccuracy: 60 }, 170 { level: 2, baseFall: 74, minSpeed: 1.6, maxSpeed: 2.0, spawnInterval: 1520, requiredAccuracy: 65 }, 171 { level: 3, baseFall: 80, minSpeed: 1.8, maxSpeed: 2.2, spawnInterval: 1400, requiredAccuracy: 70 }, 172 { level: 4, baseFall: 86, minSpeed: 2.0, maxSpeed: 2.5, spawnInterval: 1280, requiredAccuracy: 75 }, 173 { level: 5, baseFall: 92, minSpeed: 2.2, maxSpeed: 2.8, spawnInterval: 1180, requiredAccuracy: 78 }, 174 { level: 6, baseFall: 98, minSpeed: 2.38, maxSpeed: 3.02, spawnInterval: 1100, requiredAccuracy: 80 }, 175 { level: 7, baseFall: 104, minSpeed: 2.56, maxSpeed: 3.24, spawnInterval: 1020, requiredAccuracy: 82 }, 176 { level: 8, baseFall: 110, minSpeed: 2.74, maxSpeed: 3.46, spawnInterval: 940, requiredAccuracy: 83 }, 177 { level: 9, baseFall: 116, minSpeed: 2.92, maxSpeed: 3.68, spawnInterval: 880, requiredAccuracy: 85 }, 178 { level: 10, baseFall: 122, minSpeed: 3.1, maxSpeed: 3.9, spawnInterval: 820, requiredAccuracy: 85 }, 179 ]; 180 181 var PERFECT_LO = 0.93; 182 var PERFECT_HI = 1.06; 183 var GOOD_LO = 0.82; 184 var GOOD_HI = 1.18; 185 186 /** MISS: ë ¸í¸ topì´ ê¸°ì¤ì bottomì ìì í ì§ë ë¤ìë§ (client px) */ 187 var MISS_TOLERANCE = 4; 188 var FRAME_DELTA_MS_CAP = 32; 189 var KB_API_RANKING = "/api/play-lab/keyboard-beat/ranking"; 190 var KB_API_LEADERBOARD = "/api/play-lab/keyboard-beat/ranking/leaderboard"; 191 var KB_API_POSITION = "/api/play-lab/keyboard-beat/ranking/position"; 192 /** Keyboard Race ê²°ê³¼ 모ë¬ê³¼ ëì¼í íì ì´ë¦ ì ì¥ í¤ (PLAY LAB ê³µì ) */ 193 var KB_RANKING_DISPLAY_NAME_KEY = "keyboardRaceRankingDisplayNameV1"; 194 195 var SCORE_PERFECT = 100; 196 var SCORE_GOOD = 55; 197 var COMBO_BONUS = 2; 198 199 /** ëì´ëë³ ì ì ë°°ì¨ (ë 벨 scoreMultiplierì ê³±í¨) */ 200 var DIFF_SCORE_MULT = { easy: 1.0, normal: 1.3, hard: 1.6 }; 201 202 /** MISS ì ì ì ê°ì ê¸°ë³¸ê° Ã ëì´ë ë°°ì¨ */ 203 var MISS_SCORE_PENALTY_BASE = 30; 204 var DIFF_MISS_SCORE_MULT = { easy: 1.0, normal: 1.2, hard: 1.5 }; 205 206 function alphaKeys() { 207 var a = []; 208 for (var c = 65; c <= 90; c++) a.push(String.fromCharCode(c)); 209 return a; 210 } 211 212 /** 213 * Keyboard Beat ë 벨 1~10 214 * musicTrack / musicBpm: ì¶í AudioManagerë¡ ë¹í¸Â·í¸ë ë기í ìì (í ë¨ê³ììë 미ì¬ì©). 215 */ 216 var KEYBOARD_BEAT_LEVELS = [ 217 { 218 id: 1, 219 title: "ì¼ì í í¤ ê¸°ì´", 220 keys: ["A", "S", "D", "F"], 221 pattern: "repeat", 222 randomRatio: 0.3, 223 speedMultiplier: 1.0, 224 minSpeedMultiplier: 0.85, 225 maxSpeedMultiplier: 1.15, 226 spawnMultiplier: 1.0, 227 scoreMultiplier: 1.0, 228 musicTrack: "beginner_edm", 229 musicBpm: 85, 230 clearAccuracy: 70, 231 clearScore: 200, 232 }, 233 { 234 id: 2, 235 title: "ì¤ë¥¸ì í í¤ ê¸°ì´", 236 keys: ["J", "K", "L", ";"], 237 pattern: "repeat", 238 randomRatio: 0.3, 239 speedMultiplier: 1.05, 240 minSpeedMultiplier: 0.9, 241 maxSpeedMultiplier: 1.2, 242 spawnMultiplier: 0.98, 243 scoreMultiplier: 1.1, 244 musicTrack: "beginner_edm", 245 musicBpm: 90, 246 clearAccuracy: 72, 247 clearScore: 250, 248 }, 249 { 250 id: 3, 251 title: "í í¬ì§ì ì ì²´", 252 keys: ["A", "S", "D", "F", "J", "K", "L", ";"], 253 pattern: "basic", 254 randomRatio: 0.5, 255 speedMultiplier: 1.1, 256 minSpeedMultiplier: 0.95, 257 maxSpeedMultiplier: 1.3, 258 spawnMultiplier: 0.95, 259 scoreMultiplier: 1.2, 260 musicTrack: "beginner_edm", 261 musicBpm: 95, 262 clearAccuracy: 75, 263 clearScore: 300, 264 }, 265 { 266 id: 4, 267 title: "ì¢ì° ì êµì°¨", 268 keys: ["A", "S", "D", "F", "J", "K", "L", ";"], 269 pattern: "alternate", 270 randomRatio: 0.55, 271 speedMultiplier: 1.15, 272 minSpeedMultiplier: 1.0, 273 maxSpeedMultiplier: 1.4, 274 spawnMultiplier: 0.92, 275 scoreMultiplier: 1.35, 276 musicTrack: "groove_edm", 277 musicBpm: 105, 278 clearAccuracy: 78, 279 clearScore: 350, 280 }, 281 { 282 id: 5, 283 title: "í í¤ ëë¤", 284 keys: ["A", "S", "D", "F", "J", "K", "L", ";"], 285 pattern: "random_home", 286 randomRatio: 0.7, 287 speedMultiplier: 1.25, 288 minSpeedMultiplier: 1.05, 289 maxSpeedMultiplier: 1.5, 290 spawnMultiplier: 0.88, 291 scoreMultiplier: 1.5, 292 musicTrack: "groove_edm", 293 musicBpm: 110, 294 clearAccuracy: 80, 295 clearScore: 400, 296 }, 297 { 298 id: 6, 299 title: "ìì¤ í¤ ì¶ê°", 300 keys: ["Q", "W", "E", "R", "U", "I", "O", "P", "A", "S", "D", "F", "J", "K", "L", ";"], 301 pattern: "top_row", 302 randomRatio: 0.7, 303 speedMultiplier: 1.35, 304 minSpeedMultiplier: 1.1, 305 maxSpeedMultiplier: 1.6, 306 spawnMultiplier: 0.84, 307 scoreMultiplier: 1.7, 308 musicTrack: "groove_edm", 309 musicBpm: 115, 310 clearAccuracy: 82, 311 clearScore: 500, 312 }, 313 { 314 id: 7, 315 title: "ìë«ì¤ í¤ ì¶ê°", 316 keys: ["Z", "X", "C", "V", "N", "M", "A", "S", "D", "F", "J", "K", "L", ";"], 317 pattern: "bottom_row", 318 randomRatio: 0.75, 319 speedMultiplier: 1.45, 320 minSpeedMultiplier: 1.15, 321 maxSpeedMultiplier: 1.7, 322 spawnMultiplier: 0.8, 323 scoreMultiplier: 1.9, 324 musicTrack: "groove_edm", 325 musicBpm: 120, 326 clearAccuracy: 82, 327 clearScore: 600, 328 }, 329 { 330 id: 8, 331 title: "ì ì²´ ìíë²³", 332 keys: alphaKeys(), 333 pattern: "all_random", 334 randomRatio: 0.85, 335 speedMultiplier: 1.55, 336 minSpeedMultiplier: 1.2, 337 maxSpeedMultiplier: 1.85, 338 spawnMultiplier: 0.76, 339 scoreMultiplier: 2.2, 340 musicTrack: "challenge_edm", 341 musicBpm: 130, 342 clearAccuracy: 83, 343 clearScore: 750, 344 }, 345 { 346 id: 9, 347 title: "ì§§ì ë¨ì´ í¨í´", 348 keys: alphaKeys(), 349 pattern: "word", 350 randomRatio: 0.6, 351 speedMultiplier: 1.65, 352 minSpeedMultiplier: 1.3, 353 maxSpeedMultiplier: 2.0, 354 spawnMultiplier: 0.72, 355 scoreMultiplier: 2.5, 356 musicTrack: "challenge_edm", 357 musicBpm: 140, 358 clearAccuracy: 84, 359 clearScore: 900, 360 }, 361 { 362 id: 10, 363 title: "ì±ë¦°ì§", 364 keys: alphaKeys(), 365 pattern: "challenge", 366 randomRatio: 0.9, 367 speedMultiplier: 1.8, 368 minSpeedMultiplier: 1.4, 369 maxSpeedMultiplier: 2.2, 370 spawnMultiplier: 0.68, 371 scoreMultiplier: 3.0, 372 musicTrack: "challenge_edm", 373 musicBpm: 150, 374 clearAccuracy: 85, 375 clearScore: 1100, 376 }, 377 ]; 378 379 var BEAT_WORDS = ["CODE", "JAVA", "GAME", "APP", "WEB"]; 380 381 var BeatProgressStore = { 382 LS_UNLOCKED: "keyboard_beat_unlocked_level", 383 LS_SELECTED: "keyboard_beat_selected_level", 384 _clampLevel: function (n) { 385 var v = parseInt(n, 10); 386 if (isNaN(v) || v < 1) return 1; 387 if (v > 10) return 10; 388 return v; 389 }, 390 getUnlockedLevel: function () {
391 try { 392 return this._clampLevel(localStorage.getItem(this.LS_UNLOCKED)); 393 } catch (e) { 394 return 1; 395 } 396 }, 397 setUnlockedLevel: function (level) { 398 try { 399 localStorage.setItem(this.LS_UNLOCKED, String(this._clampLevel(level))); 400 } catch (e) { 401 /* ignore */ 402 } 403 }, 404 getSelectedLevel: function () { 405 try { 406 return this._clampLevel(localStorage.getItem(this.LS_SELECTED)); 407 } catch (e) { 408 return 1; 409 } 410 }, 411 setSelectedLevel: function (level) { 412 try { 413 localStorage.setItem(this.LS_SELECTED, String(this._clampLevel(level))); 414 } catch (e) { 415 /* ignore */ 416 } 417 }, 418 resetProgress: function () { 419 try { 420 localStorage.removeItem(this.LS_UNLOCKED); 421 localStorage.removeItem(this.LS_SELECTED); 422 } catch (e) { 423 /* ignore */ 424 } 425 }, 426 }; 427 428 function getLevelConfig(levelNum) { 429 var n = BeatProgressStore._clampLevel(levelNum); 430 return KEYBOARD_BEAT_LEVELS[n - 1] || KEYBOARD_BEAT_LEVELS[0]; 431 } 432 433 function getBeatGameplayConfig(levelNum) { 434 var n = parseInt(levelNum, 10); 435 if (isNaN(n) || n < 1) n = 1; 436 if (n > BEAT_LEVELS.length) return BEAT_LEVELS[BEAT_LEVELS.length - 1]; 437 return BEAT_LEVELS[n - 1]; 438 } 439 440 function getCurrentBeatGameplayConfig() { 441 return getBeatGameplayConfig(state.currentLevel); 442 } 443 444 function travelMsFromFallSpeed(fallSpeed) { 445 var fs = fallSpeed > 0 ? fallSpeed : 70; 446 return Math.round(FALL_SPEED_TRAVEL_NUMERATOR / fs); 447 } 448 449 function keysForBeatLevel(levelNum) { 450 var n = BeatProgressStore._clampLevel(levelNum); 451 if (n >= 8) return alphaKeys(); 452 return BEAT_KEY_PROGRESSION[n - 1] ? BEAT_KEY_PROGRESSION[n - 1].slice() : BEAT_KEY_PROGRESSION[0].slice(); 453 } 454 455 function clampBeatSpeedBias() { 456 var gp = getCurrentBeatGameplayConfig(); 457 if (state.speedBias == null || isNaN(state.speedBias)) { 458 state.speedBias = (gp.minSpeed + gp.maxSpeed) / 2; 459 } 460 state.speedBias = Math.max(gp.minSpeed, Math.min(gp.maxSpeed, state.speedBias)); 461 var effectiveFall = gp.baseFall * state.speedBias; 462 state.travelMs = travelMsFromFallSpeed(effectiveFall); 463 } 464 465 function nudgeBeatSpeedBias(correct) { 466 var gp = getCurrentBeatGameplayConfig(); 467 state.speedBias += correct ? 0.02 : -0.03; 468 clampBeatSpeedBias(); 469 beatDebugLog("speedBias", state.speedBias, "travelMs", state.travelMs, "level", state.currentLevel); 470 } 471 472 /** ì¤í ì´ì§Â·ì¤ì #kbHitLine DOMë§ ì¬ì© (ë¼ë²¨/ë´ë¶ ì¶ì ì¢íë¡ MISS íì íì§ ìì) */ 473 function readBeatJudgementRects() { 474 if (!els.laneStage || !els.hitLine) return null; 475 var stageR = els.laneStage.getBoundingClientRect(); 476 var lineR = els.hitLine.getBoundingClientRect(); 477 if (stageR.height < 8 || lineR.height < 1 || lineR.width < 8) return null; 478 var spawnCenterY = Math.max(14, stageR.height * 0.06); 479 var hitCenterY = lineR.top - stageR.top + lineR.height * 0.5; 480 return { 481 stageTop: stageR.top, 482 stageHeight: stageR.height, 483 spawnCenterY: spawnCenterY, 484 hitCenterY: hitCenterY, 485 lineTop: lineR.top, 486 lineBottom: lineR.bottom, 487 }; 488 } 489 490 function updateBeatPlayfieldMetrics() { 491 state.beatPf = readBeatJudgementRects(); 492 return state.beatPf; 493 } 494 495 function applyBeatNoteTravelAnchors(note) { 496 var pf = readBeatJudgementRects(); 497 if (!pf) return; 498 note.spawnCenterY = pf.spawnCenterY; 499 note.hitCenterY = pf.hitCenterY; 500 } 501 502 /** ì¤í ì´ì§ ìë ë ¸í¸ ì¤ì¬ Y (getBoundingClientRect + translate(-50%,-50%) ë°ì) */ 503 function noteCenterYStageRel(note) { 504 if (!note || !note.el || !els.laneStage) return null; 505 var noteR = note.el.getBoundingClientRect(); 506 var stageR = els.laneStage.getBoundingClientRect(); 507 if (noteR.height < 1 || stageR.height < 8) return null; 508 return noteR.top + noteR.height * 0.5 - stageR.top; 509 } 510 511 /** 512 * ë ì´ìì ë³ê²½ í: ì spawn/hit ìµì»¤ì ë§ì¶° progressë§ ìì°í´ íë©´ì ì¤ì¬ ì ì§ (ì í ë°©ì§) 513 */ 514 function rebalanceNoteProgressFromDom(note, pf) { 515 if (!note || !pf) return false;
516 var centerRel = noteCenterYStageRel(note); 517 if (centerRel == null) return false; 518 var spawnY = pf.spawnCenterY; 519 var hitY = pf.hitCenterY; 520 var track = hitY - spawnY; 521 if (Math.abs(track) < 1) { 522 note.spawnCenterY = spawnY; 523 note.hitCenterY = hitY; 524 return false; 525 } 526 var p = (centerRel - spawnY) / track; 527 if (p < 0) p = 0; 528 note.progress = p; 529 note.spawnCenterY = spawnY; 530 note.hitCenterY = hitY; 531 return true; 532 } 533 534 function positionBeatNoteElement(note) { 535 if (!note.el) return; 536 var spawnY = note.spawnCenterY; 537 var hitY = note.hitCenterY; 538 if (spawnY == null || hitY == null) { 539 applyBeatNoteTravelAnchors(note); 540 spawnY = note.spawnCenterY; 541 hitY = note.hitCenterY; 542 } 543 if (spawnY == null || hitY == null) return; 544 var p = note.progress != null ? note.progress : 0; 545 var centerY = spawnY + p * (hitY - spawnY); 546 note.el.style.top = centerY + "px"; 547 } 548 549 function logKbLayout(reason) { 550 if (!BEAT_DEBUG) return; 551 var pf = state.beatPf || readBeatJudgementRects(); 552 var payload = { 553 reason: reason, 554 stageHeight: pf ? pf.stageHeight : null, 555 lineTop: pf ? pf.lineTop : null, 556 lineBottom: pf ? pf.lineBottom : null, 557 activeNotes: state.notes ? state.notes.length : 0, 558 }; 559 console.log("[KB LAYOUT] " + beatStringifySafe(payload)); 560 } 561 562 function logKbMissEvent(source, note) { 563 if (!BEAT_DEBUG) return; 564 var payload = { 565 source: source, 566 key: note && note.key, 567 level: state.currentLevel, 568 progress: note && note.progress, 569 spawnCenterY: note && note.spawnCenterY, 570 hitCenterY: note && note.hitCenterY, 571 travelMsForNote: note && note.travelMsForNote, 572 rafId: state.rafId, 573 notesCount: state.notes ? state.notes.length : 0, 574 }; 575 if (note && note.el && els.hitLine) { 576 var noteR = note.el.getBoundingClientRect(); 577 var lineR = els.hitLine.getBoundingClientRect(); 578 payload.noteTop = noteR.top; 579 payload.noteBottom = noteR.bottom; 580 payload.lineTop = lineR.top; 581 payload.lineBottom = lineR.bottom; 582 payload.lineCenter = lineR.top + lineR.height / 2; 583 } 584 console.log("[KB MISS] " + beatStringifySafe(payload)); 585 } 586 587 function getNoteLineDistance(note) { 588 if (!note || !note.el || !els.hitLine) return 9999; 589 var noteR = note.el.getBoundingClientRect(); 590 var lineR = els.hitLine.getBoundingClientRect(); 591 if (noteR.height < 1 || lineR.height < 1) return 9999; 592 var noteCenterY = noteR.top + noteR.height / 2; 593 var lineCenterY = lineR.top + lineR.height / 2; 594 return noteCenterY - lineCenterY; 595 } 596 597 /** HIT ìëì° â ì¤í ì´ì§ ëì´ ë¹ì¨ + min/max (PC·모ë°ì¼ ê³µíµ) */ 598 function getBeatHitWindows() { 599 var stageR = els.laneStage ? els.laneStage.getBoundingClientRect() : null; 600 var h = stageR && stageR.height > 0 ? stageR.height : 700; 601 return { 602 perfect: Math.max(24, Math.min(34, h * 0.035)), 603 good: Math.max(56, Math.min(78, h * 0.085)), 604 }; 605 } 606 607 /** HIT íë³´: DOM 기ì¤ì ì¤ì¬ê³¼ì 거리 (progress ë³´ì¡° ìì´ ìµì¢ íì ) */ 608 function isNoteInHitWindow(note) { 609 if (!note || !note.el || !els.hitLine) return null; 610 var distance = Math.abs(getNoteLineDistance(note)); 611 var windows = getBeatHitWindows(); 612 if (distance <= windows.perfect) return "perfect"; 613 if (distance <= windows.good) return "good"; 614 return null; 615 } 616 617 /** ë ì´ìì ë³ê²½: DOMì ë ¸í¸ ì¤ì¬ ì ì§íë©° ìµì»¤Â·progress ì¬ì ë ¬ */ 618 function refreshActiveNoteAnchors(reason) { 619 if (!Array.isArray(state.notes)) return; 620 var pf = updateBeatPlayfieldMetrics(); 621 if (!pf) return; 622 for (var i = 0; i < state.notes.length; i++) { 623 var note = state.notes[i]; 624 if (!note || note.judged || !note.el) continue; 625 if (!rebalanceNoteProgressFromDom(note, pf)) { 626 applyBeatNoteTravelAnchors(note); 627 } 628 positionBeatNoteElement(note); 629 } 630 logKbLayout(reason || "refreshActiveNoteAnchors"); 631 } 632 633 /** MISS: ë ¸í¸ê° 기ì¤ì ì ìì í ì§ë ë¤ë§ (ë¿ê±°ë ê±¸ì¹ ìíë MISS ìë) */ 634 function notePassedBeatMissLine(note) { 635 if (!note || !note.el || !els.hitLine) return false;
636 var noteR = note.el.getBoundingClientRect(); 637 var lineR = els.hitLine.getBoundingClientRect(); 638 if (noteR.height < 1 || lineR.height < 1) return false; 639 return noteR.top > lineR.bottom + MISS_TOLERANCE; 640 } 641 642 function syncBeatPlayfieldLayout(repositionNotes, layoutReason) { 643 updateBeatPlayfieldMetrics(); 644 if (repositionNotes) { 645 refreshActiveNoteAnchors(layoutReason || "syncBeatPlayfieldLayout"); 646 } 647 } 648 649 function queueBeatPlayfieldLayoutSync(repositionNotes, layoutReason) { 650 var reason = layoutReason || (repositionNotes ? "queueSync:true" : "queueSync:false"); 651 var run = function () { 652 syncBeatPlayfieldLayout(repositionNotes, reason); 653 }; 654 requestAnimationFrame(function () { 655 run(); 656 requestAnimationFrame(function () { 657 run(); 658 requestAnimationFrame(run); 659 }); 660 }); 661 } 662 663 function cancelBeatFrame() { 664 if (state.rafId) { 665 cancelAnimationFrame(state.rafId); 666 state.rafId = 0; 667 } 668 } 669 670 function requestBeatFrame() { 671 cancelBeatFrame(); 672 state.lastFramePerf = performance.now(); 673 state.rafId = requestAnimationFrame(tickFrame); 674 } 675 676 function scheduleNextBeatFrame() { 677 state.rafId = requestAnimationFrame(tickFrame); 678 } 679 680 /* -------------------------------------------------------------------------- */ 681 /* Sound engine (Web Audio API) */ 682 /* -------------------------------------------------------------------------- */ 683 684 var Sound = { 685 ctx: null, 686 master: null, 687 688 init: function () { 689 var Ctx = window.AudioContext || window.webkitAudioContext; 690 if (!Ctx) return null; 691 if (!this.ctx) { 692 this.ctx = new Ctx(); 693 this.master = this.ctx.createGain(); 694 this.master.gain.value = 0.45; 695 this.master.connect(this.ctx.destination); 696 } 697 return this.ctx; 698 }, 699 700 resume: function () { 701 if (this.ctx && this.ctx.state === "suspended") return this.ctx.resume(); 702 return Promise.resolve(); 703 }, 704 705 _now: function () { 706 return this.ctx ? this.ctx.currentTime : 0; 707 }, 708 709 /** ì§§ì í¥/í± â ë¹í¸ì© */ 710 beatTick: function () { 711 if (!this.ctx || !this.master) return; 712 var t = this._now(); 713 var osc = this.ctx.createOscillator(); 714 var g = this.ctx.createGain(); 715 var filter = this.ctx.createBiquadFilter(); 716 filter.type = "lowpass"; 717 filter.frequency.value = 420; 718 osc.type = "triangle"; 719 osc.frequency.setValueAtTime(180, t); 720 osc.frequency.exponentialRampToValueAtTime(55, t + 0.06); 721 g.gain.setValueAtTime(0.22, t); 722 g.gain.exponentialRampToValueAtTime(0.0008, t + 0.09); 723 osc.connect(filter); 724 filter.connect(g); 725 g.connect(this.master); 726 osc.start(t); 727 osc.stop(t + 0.1); 728 }, 729 730 /** Perfect â ë°ê³ ì§§ì í */ 731 hitPopPerfect: function () { 732 if (!this.ctx || !this.master) return; 733 var t = this._now(); 734 var osc = this.ctx.createOscillator(); 735 var g = this.ctx.createGain(); 736 osc.type = "sine"; 737 osc.frequency.setValueAtTime(720, t); 738 osc.frequency.exponentialRampToValueAtTime(1100, t + 0.045); 739 g.gain.setValueAtTime(0.22, t); 740 g.gain.exponentialRampToValueAtTime(0.0008, t + 0.075); 741 osc.connect(g); 742 g.connect(this.master); 743 osc.start(t); 744 osc.stop(t + 0.085); 745 }, 746 747 /** Good â ì½ê° ë®ê³ ì§§ê² */ 748 hitPopGood: function () { 749 if (!this.ctx || !this.master) return; 750 var t = this._now(); 751 var osc = this.ctx.createOscillator(); 752 var g = this.ctx.createGain(); 753 osc.type = "triangle"; 754 osc.frequency.setValueAtTime(420, t); 755 osc.frequency.exponentialRampToValueAtTime(620, t + 0.05); 756 g.gain.setValueAtTime(0.14, t); 757 g.gain.exponentialRampToValueAtTime(0.0008, t + 0.08); 758 osc.connect(g); 759 g.connect(this.master); 760 osc.start(t); 761 osc.stop(t + 0.09); 762 }, 763 764 /** Miss â ë®ì ë²ì¦ */ 765 missBuzz: function () { 766 if (!this.ctx || !this.master) return; 767 var t = this._now(); 768 var osc = this.ctx.createOscillator(); 769 var g = this.ctx.createGain(); 770 osc.type = "sawtooth"; 771 osc.frequency.setValueAtTime(140, t); 772 osc.frequency.linearRampToValueAtTime(90, t + 0.12); 773 g.gain.setValueAtTime(0.12, t); 774 g.gain.exponentialRampToValueAtTime(0.0008, t + 0.14); 775 osc.connect(g); 776 g.connect(this.master); 777 osc.start(t); 778 osc.stop(t + 0.15); 779 }, 780 }; 781 782 /* -------------------------------------------------------------------------- */ 783 /* Game state */ 784 /* -------------------------------------------------------------------------- */ 785 786 var state = { 787 phase: "idle", 788 bpm: 90, 789 keys: [], 790 notes: [], 791 gameStartPerf: 0, 792 rafId: 0, 793 lastFramePerf: 0, 794 beatTimerId: 0, 795 timeLeftId: 0, 796 score: 0, 797 combo: 0, 798 perfect: 0, 799 good: 0, 800 miss: 0, 801 idSeq: 0, 802 maxCombo: 0,
803 endReason: "", // "time_up" 804 lastDurationMs: 0, 805 beatRankingSaveState: "idle", // idle | pending | success | failed 806 lastResultAccNum: 0, 807 /** ëí¹ payloadì©: ë¼ì´ë ì¤ í°ì¹ ì ë ¥ì´ ìì¼ë©´ "touch", ìëë©´ "keyboard" */ 808 inputMode: "keyboard", 809 selectedLevel: 1, 810 unlockedLevel: 1, 811 /** íì¬ ì í ë 벨ì KEYBOARD_BEAT_LEVELS í목 참조 */ 812 levelConfig: null, 813 /** ìº íì¸ ì§í ì¤ íì¬ ë 벨 (1ë¶í°) */ 814 currentLevel: 1, 815 maxLevelReached: 1, 816 levelCorrectCount: 0, 817 levelMissCount: 0, 818 levelTotalInputCount: 0, 819 levelStartPerf: 0, 820 levelEndPerf: 0, 821 levelNextSpawnPerf: 0, 822 levelClearTimerId: 0, 823 patternCtx: null, 824 spawnSeqIndex: 0, 825 sessionLevelsCleared: 0, 826 /** ë ¸í¸ ëí ìê° (ë 벨 fallSpeed 기ë°) */ 827 travelMs: TRAVEL_MS, 828 /** ë 벨 ë´ ëì ìë ë°°ì¨ (minSpeed~maxSpeed) */ 829 speedBias: null, 830 speedBiasLevel: 0, 831 /** íì ì ·ì¤í ì´ì§ DOM ë©í¸ë¦ (매 íë ì/리ì¬ì´ì¦ ê°±ì ) */ 832 beatPf: null, 833 /** ì§ì ë¼ì´ë í´ë¦¬ì´ ì¬ë¶ (ê²°ê³¼ UI) */ 834 lastRoundCleared: false, 835 endGameMeta: null, 836 }; 837 838 var els = { 839 laneColBgs: [], 840 lanePressTimeouts: [], 841 }; 842 843 /** 844 * 모ë°ì¼: Fullscreen ìì²(+ì¤í¨ ì 100dvh ê³ ì ë ì´ìì)·body ì¤í¬ë¡¤ ì ê¸ 845 * PC : íë ì´ ìì ì íë¡í UI ì¨ê¹ë§ (ë ì´ìì·ì ì²´ íë©´ ìì) 846 */ 847 var beatChrome = { 848 scrollLocked: false, 849 savedY: 0, 850 fsListenersBound: false, 851 /** 모ë°ì¼: Start~ë¼ì´ë ì¢ ë£ê¹ì§(ì¹´ì´í¸ë¤ì´ í¬í¨) 몰ì UI ì ì§ íëê·¸ */ 852 immersiveSession: false, 853 854 isMobileKb: function () { 855 try { 856 return window.matchMedia("(max-width: 900px)").matches; 857 } catch (eMq) { 858 return false; 859 } 860 }, 861 862 getGameEl: function () { 863 return els.game || document.getElementById("kbGame"); 864 }, 865 866 /** iPhone/iPad ë±: íì¤ Fullscreen 미ì§ì·ì¤í¨í´ë fixed ì¤ë²ë ì´ë¡ ì¶©ë¶ (Androidë ì íì FS ìë ì ì§) */ 867 shouldSkipFullscreenApi: function () { 868 try { 869 var ua = navigator.userAgent || ""; 870 if (/iPhone|iPod/.test(ua)) return true; 871 if (/iPad/.test(ua)) return true; 872 if (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) return true; 873 return false; 874 } catch (eIos) { 875 return false; 876 } 877 }, 878 879 fullscreenElementNow: function () { 880 try { 881 return document.fullscreenElement || document.webkitFullscreenElement || null; 882 } catch (e) { 883 return null; 884 } 885 }, 886 887 bindFullscreenFallbackListenersOnce: function () { 888 var self = this; 889 if (self.fsListenersBound) return; 890 self.fsListenersBound = true; 891 892 function onFsChange() { 893 try { 894 if (!self.isMobileKb() || !self.immersiveSession) return; 895 var ge = self.getGameEl(); 896 var fsNow = self.fullscreenElementNow(); 897 if (!ge || fsNow !== ge) self.lockBodyScrollMobile(); 898 } catch (eFs) {} 899 } 900 901 document.addEventListener("fullscreenchange", onFsChange); 902 document.addEventListener("webkitfullscreenchange", onFsChange); 903 }, 904 905 requestFullscreenSafe: function (el) { 906 if (!el) return null; 907 try { 908 if (typeof el.requestFullscreen === "function") { 909 var req = null; 910 try { 911 req = el.requestFullscreen({ navigationUI: "hide" }); 912 } catch (eNav) { 913 try { 914 req = el.requestFullscreen(); 915 } catch (eBare) {} 916 } 917 if (req != null && typeof req.catch === "function") { 918 req.catch(function () {}); 919 } 920 return req; 921 } 922 } catch (eReq) {} 923 924 try { 925 if (typeof el.webkitRequestFullscreen === "function") { 926 el.webkitRequestFullscreen(); 927 } 928 } catch (eWeb) {} 929 930 return null; 931 }, 932 933 exitFullscreenSafe: function () { 934 try { 935 if (!this.fullscreenElementNow()) return; 936 if (typeof document.exitFullscreen === "function") document.exitFullscreen(); 937 else if (typeof document.webkitExitFullscreen === "function") document.webkitExitFullscreen(); 938 } catch (eEx) {} 939 }, 940 941 lockBodyScrollMobile: function () { 942 if (!this.isMobileKb() || this.scrollLocked) return; 943 this.scrollLocked = true; 944 this.savedY = window.pageYOffset || document.documentElement.scrollTop || 0; 945 document.body.style.position = "fixed"; 946 document.body.style.top = "-" + this.savedY + "px"; 947 document.body.style.left = "0"; 948 document.body.style.right = "0"; 949 document.body.style.width = "100%"; 950 document.body.style.overflow = "hidden"; 951 document.documentElement.style.overflow = "hidden";
952 try { 953 document.body.style.overscrollBehavior = "none"; 954 document.documentElement.style.overscrollBehavior = "none"; 955 } catch (eOs) {} 956 }, 957 958 unlockBodyScrollSafe: function () { 959 if (!this.scrollLocked) return; 960 this.scrollLocked = false; 961 document.body.style.position = ""; 962 document.body.style.top = ""; 963 document.body.style.left = ""; 964 document.body.style.right = ""; 965 document.body.style.width = ""; 966 document.body.style.overflow = ""; 967 document.documentElement.style.overflow = ""; 968 try { 969 document.body.style.overscrollBehavior = ""; 970 document.documentElement.style.overscrollBehavior = ""; 971 } catch (eOs2) {} 972 window.scrollTo(0, this.savedY); 973 }, 974 975 enterMobilePrepFromUserGesture: function () { 976 if (!this.isMobileKb()) return; 977 updateAppHeight(); 978 this.immersiveSession = true; 979 document.body.classList.add("kb-mobile-playing"); 980 document.body.classList.add("pl-game-focus-mode"); 981 var ge = this.getGameEl(); 982 if (ge) ge.classList.add("kb-game-immersive"); 983 this.bindFullscreenFallbackListenersOnce(); 984 this.lockBodyScrollMobile(); 985 /* fixed ì ì²´íë©´ì´ ê¸°ë³¸.iOS ë±ì ì¬ê¸°ë§ì¼ë¡ 충족.Android ë±ì ì íì ì¼ë¡ FS ìë. */ 986 if (!this.shouldSkipFullscreenApi()) { 987 this.requestFullscreenSafe(ge); 988 } 989 }, 990 991 retryFullscreenMobile: function () { 992 try { 993 if (!this.isMobileKb() || this.shouldSkipFullscreenApi()) return; 994 this.requestFullscreenSafe(this.getGameEl()); 995 } catch (eRy) {} 996 }, 997 998 activatePcPlayingChromeOnly: function () { 999 document.body.classList.add("pl-game-focus-mode"); 1000 }, 1001 1002 teardown: function () { 1003 this.immersiveSession = false; 1004 1005 try { 1006 this.exitFullscreenSafe(); 1007 } catch (eT) {} 1008 1009 document.body.classList.remove("kb-mobile-playing"); 1010 document.body.classList.remove("pl-game-focus-mode"); 1011 try { 1012 var ge = this.getGameEl(); 1013 if (ge) ge.classList.remove("kb-game-immersive"); 1014 } catch (eR) {} 1015 1016 try { 1017 this.unlockBodyScrollSafe(); 1018 } catch (eU) {} 1019 }, 1020 }; 1021 1022 function currentTravelMs() { 1023 return state.travelMs > 0 ? state.travelMs : TRAVEL_MS; 1024 } 1025 1026 function applyTravelMsFromGameplayConfig() { 1027 var gp = getCurrentBeatGameplayConfig(); 1028 if (state.speedBiasLevel !== state.currentLevel || state.speedBias == null) { 1029 state.speedBias = (gp.minSpeed + gp.maxSpeed) / 2; 1030 state.speedBiasLevel = state.currentLevel; 1031 } 1032 clampBeatSpeedBias(); 1033 } 1034 1035 function getSelectedLevelConfig() { 1036 return getLevelConfig(state.selectedLevel); 1037 } 1038 1039 function resolveBeatKeysForLevel(levelNum) { 1040 var cfg = getLevelConfig(levelNum); 1041 state.levelConfig = cfg; 1042 state.keys = normalizeBeatKeysList(keysForBeatLevel(levelNum)); 1043 beatDebugLog("keyPool level", levelNum, state.keys); 1044 } 1045 1046 function resolveBeatKeys() { 1047 resolveBeatKeysForLevel(state.selectedLevel); 1048 } 1049 1050 function getLevelAccuracy() { 1051 if (state.levelTotalInputCount <= 0) return 0; 1052 return Math.round((state.levelCorrectCount / state.levelTotalInputCount) * 100); 1053 } 1054 1055 function resetLevelCounters() { 1056 state.levelCorrectCount = 0; 1057 state.levelMissCount = 0; 1058 state.levelTotalInputCount = 0; 1059 } 1060 1061 function recordLevelInput(correct) { 1062 state.levelTotalInputCount += 1; 1063 if (correct) state.levelCorrectCount += 1; 1064 else state.levelMissCount += 1; 1065 if (state.phase === "playing") nudgeBeatSpeedBias(correct); 1066 } 1067 1068 /** ë 벨·ì¤ì¼ì¤Â·íì ì ì°ë í¤ ë°°ì´ â ë¼í´ ìíë²³ì ë문ìë¡ íµì¼ */ 1069 function normalizeBeatKeysList(keys) { 1070 var out = []; 1071 var seen = {}; 1072 if (!keys || !keys.length) return out; 1073 for (var i = 0; i < keys.length; i++) { 1074 var slot = canonicalBeatKeyFromAllow(keys[i], keys); 1075 if (!slot) slot = normalizeBeatKeyCompareUnit(keys[i]); 1076 if (!slot || seen[slot]) continue; 1077 seen[slot] = true; 1078 out.push(slot); 1079 } 1080 return out; 1081 } 1082 1083 /** allow ë°°ì´ ììì rawì ëì¼ í¤(ëì문ì 무ì)ì íì¤ ë¬¸ìì´ */ 1084 function canonicalBeatKeyFromAllow(raw, allow) { 1085 var wantNorm = normalizeBeatKeyCompareUnit(raw); 1086 if (!wantNorm || !allow || !allow.length) return ""; 1087 for (var i = 0; i < allow.length; i++) { 1088 if (normalizeBeatKeyCompareUnit(allow[i]) === wantNorm) { 1089 return normalizeBeatKeyCompareUnit(allow[i]); 1090 } 1091 } 1092 return ""; 1093 } 1094 1095 function difficultyScoreMultiplier() { 1096 var slug = readDifficultySlug(); 1097 return DIFF_SCORE_MULT[slug] != null ? DIFF_SCORE_MULT[slug] : DIFF_SCORE_MULT.easy; 1098 } 1099 1100 function levelScoreMultiplier() { 1101 return state.levelConfig && state.levelConfig.scoreMultiplier != null ? state.levelConfig.scoreMultiplier : 1; 1102 } 1103 1104 function pickRandomKeyFrom(arr) { 1105 var a = arr && arr.length ? arr : state.keys; 1106 if (!a || !a.length) return "A"; 1107 return a[Math.floor(Math.random() * a.length)]; 1108 } 1109 1110 /** ì¤ì¼ì¤ ë ¸í¸ì©: ë°ëì allow ë°°ì´ ìì í¤ë§ ë°í (ëì문ì 무ì) */ 1111 function ensureScheduleKey(key, allow) { 1112 var canon = canonicalBeatKeyFromAllow(key, allow); 1113 if (canon) return canon; 1114 return pickRandomKeyFrom(allow); 1115 } 1116 1117 function filterWordsForLevelKeys(words, keySet) { 1118 var out = []; 1119 if (!words || !words.length || !keySet || !keySet.length) return out; 1120 for (var wi = 0; wi < words.length; wi++) { 1121 var w = words[wi]; 1122 if (!w || !w.length) continue; 1123 var ok = true; 1124 for (var ci = 0; ci < w.length; ci++) { 1125 if (keySet.indexOf(w.charAt(ci)) < 0) { 1126 ok = false;
1127 break; 1128 } 1129 } 1130 if (ok) out.push(w); 1131 } 1132 return out; 1133 } 1134 1135 function createPatternContext(levelConfig) { 1136 var keys = normalizeBeatKeysList(levelConfig.keys); 1137 return { 1138 keys: keys, 1139 seqPos: 0, 1140 wordIndex: 0, 1141 wordCharIndex: 0, 1142 words: filterWordsForLevelKeys(BEAT_WORDS.slice(), keys), 1143 challengeMode: 0, 1144 _repeatSeq: null, 1145 _altSeq: null, 1146 _homeSeq: null, 1147 }; 1148 } 1149 1150 function pickPatternKey(levelConfig, ctx, index) { 1151 var keys = ctx.keys; 1152 var pattern = levelConfig.pattern; 1153 var allow = levelConfig.keys; 1154 if (!keys.length) return ensureScheduleKey("A", allow); 1155 1156 var raw = ""; 1157 1158 if (pattern === "repeat") { 1159 if (!ctx._repeatSeq) { 1160 ctx._repeatSeq = []; 1161 for (var i = 0; i < keys.length; i++) { 1162 ctx._repeatSeq.push(keys[i], keys[i]); 1163 } 1164 } 1165 raw = ctx._repeatSeq[ctx.seqPos % ctx._repeatSeq.length]; 1166 ctx.seqPos += 1; 1167 return ensureScheduleKey(raw, allow); 1168 } 1169 1170 if (pattern === "basic") { 1171 raw = keys[index % keys.length]; 1172 ctx.seqPos += 1; 1173 return ensureScheduleKey(raw, allow); 1174 } 1175 1176 if (pattern === "alternate") { 1177 if (!ctx._altSeq) { 1178 var altFull = ["A", "J", "S", "K", "D", "L", "F", ";"]; 1179 ctx._altSeq = []; 1180 for (var ai = 0; ai < altFull.length; ai++) { 1181 if (keys.indexOf(altFull[ai]) >= 0) ctx._altSeq.push(altFull[ai]); 1182 } 1183 } 1184 if (!ctx._altSeq.length) return pickRandomKeyFrom(allow); 1185 raw = ctx._altSeq[ctx.seqPos % ctx._altSeq.length]; 1186 ctx.seqPos += 1; 1187 return ensureScheduleKey(raw, allow); 1188 } 1189 1190 if (pattern === "random_home") { 1191 if (!ctx._homeSeq) { 1192 var homeFull = ["A", "S", "D", "F", "J", "K", "L", ";"]; 1193 ctx._homeSeq = []; 1194 for (var hi = 0; hi < homeFull.length; hi++) { 1195 if (keys.indexOf(homeFull[hi]) >= 0) ctx._homeSeq.push(homeFull[hi]); 1196 } 1197 } 1198 if (!ctx._homeSeq.length) return pickRandomKeyFrom(allow); 1199 raw = ctx._homeSeq[ctx.seqPos % ctx._homeSeq.length]; 1200 ctx.seqPos += 1; 1201 return ensureScheduleKey(raw, allow); 1202 } 1203 1204 if (pattern === "top_row" || pattern === "bottom_row") { 1205 raw = keys[ctx.seqPos % keys.length]; 1206 ctx.seqPos += 1; 1207 return ensureScheduleKey(raw, allow); 1208 } 1209 1210 if (pattern === "all_random") { 1211 raw = keys[ctx.seqPos % keys.length]; 1212 ctx.seqPos += 1; 1213 return ensureScheduleKey(raw, allow); 1214 } 1215 1216 if (pattern === "word") { 1217 if (!ctx.words.length) return pickRandomKeyFrom(allow); 1218 var w = ctx.words[ctx.wordIndex % ctx.words.length]; 1219 raw = w.charAt(ctx.wordCharIndex); 1220 ctx.wordCharIndex += 1; 1221 if (ctx.wordCharIndex >= w.length) { 1222 ctx.wordCharIndex = 0; 1223 ctx.wordIndex += 1; 1224 } 1225 return ensureScheduleKey(raw, allow); 1226 } 1227 1228 if (pattern === "challenge") { 1229 var m = index % 7; 1230 if (m === 0 || m === 1) { 1231 return pickRandomKeyFrom(allow); 1232 } 1233 if (m >= 2 && m <= 4) { 1234 if (!ctx.words.length) return pickRandomKeyFrom(allow); 1235 var w2 = ctx.words[ctx.wordIndex % ctx.words.length]; 1236 raw = w2.charAt(ctx.wordCharIndex); 1237 ctx.wordCharIndex += 1; 1238 if (ctx.wordCharIndex >= w2.length) { 1239 ctx.wordCharIndex = 0; 1240 ctx.wordIndex += 1; 1241 } 1242 return ensureScheduleKey(raw, allow); 1243 } 1244 raw = keys[ctx.seqPos % keys.length]; 1245 ctx.seqPos += 1; 1246 return ensureScheduleKey(raw, allow); 1247 } 1248 1249 raw = keys[ctx.seqPos++ % keys.length]; 1250 return ensureScheduleKey(raw, allow); 1251 } 1252 1253 function laneIndexForKey(key) { 1254 var canon = canonicalBeatLaneKey(key); 1255 if (canon) return state.keys.indexOf(canon); 1256 return -1; 1257 } 1258 1259 function resetRoundStats() { 1260 state.score = 0; 1261 state.currentLevel = 1; 1262 state.maxLevelReached = 1; 1263 state.sessionLevelsCleared = 0; 1264 resetLevelCounters(); 1265 state.combo = 0; 1266 state.perfect = 0; 1267 state.good = 0; 1268 state.miss = 0; 1269 state.maxCombo = 0; 1270 state.endReason = ""; 1271 state.beatRankingSaveState = "idle"; 1272 state.inputMode = "keyboard"; 1273 } 1274 1275 function resetState() { 1276 state.phase = "idle"; 1277 resetRoundStats(); 1278 cancelBeatFrame(); 1279 if (state.beatTimerId) { 1280 clearInterval(state.beatTimerId); 1281 state.beatTimerId = 0; 1282 } 1283 if (state.levelClearTimerId) { 1284 clearTimeout(state.levelClearTimerId); 1285 state.levelClearTimerId = 0; 1286 } 1287 if (state.timeLeftId) { 1288 clearInterval(state.timeLeftId); 1289 state.timeLeftId = 0; 1290 } 1291 state.patternCtx = null; 1292 state.spawnSeqIndex = 0; 1293 state.lastFramePerf = 0; 1294 hideLevelClearOverlay(); 1295 clearPlayfieldNotes(); 1296 if (els.boardFx) { 1297 els.boardFx.querySelectorAll(".kb-center-judge, .kb-hit-burst").forEach(function (n) { n.remove(); }); 1298 } 1299 if (els.lanePressTimeouts && els.lanePressTimeouts.length) {
1300 els.lanePressTimeouts.forEach(function (tid) { 1301 if (tid) clearTimeout(tid); 1302 }); 1303 els.lanePressTimeouts = []; 1304 } 1305 } 1306 1307 /* -------------------------------------------------------------------------- */ 1308 /* Scoring */ 1309 /* -------------------------------------------------------------------------- */ 1310 1311 function updatePlayHud() { 1312 if (els.playScore) els.playScore.textContent = String(state.score); 1313 if (els.playComboHud) els.playComboHud.textContent = String(state.combo); 1314 var gp = getCurrentBeatGameplayConfig(); 1315 var acc = getLevelAccuracy(); 1316 if (els.kbPlayAccuracy) els.kbPlayAccuracy.textContent = acc + "%"; 1317 if (els.kbPlayTargetAccuracy) els.kbPlayTargetAccuracy.textContent = gp.requiredAccuracy + "%"; 1318 updatePlayHudLevelBrief(); 1319 refreshComboDock(); 1320 } 1321 1322 /** ë ¸í¸ MISS ëë ì못ë í¤ MISS â ì ì ê°ì ·ìë íí¥, ë¼ì´ëë ìê°ê¹ì§ ì ì§ */ 1323 function penalizeMiss(laneIdx, meta) { 1324 meta = meta || {}; 1325 if (BEAT_DEBUG && meta.source === "wrong_key") { 1326 console.log("[KB MISS] " + beatStringifySafe({ 1327 source: "wrong_key", 1328 laneIdx: laneIdx, 1329 level: state.currentLevel, 1330 rafId: state.rafId, 1331 notesCount: state.notes ? state.notes.length : 0, 1332 })); 1333 } 1334 state.miss += 1; 1335 recordLevelInput(false); 1336 state.combo = 0; 1337 var slug = readDifficultySlug(); 1338 var dm = DIFF_MISS_SCORE_MULT[slug] != null ? DIFF_MISS_SCORE_MULT[slug] : DIFF_MISS_SCORE_MULT.easy; 1339 var penalty = Math.round(MISS_SCORE_PENALTY_BASE * dm); 1340 state.score = Math.max(0, state.score - penalty); 1341 Sound.missBuzz(); 1342 boardFlashMiss(); 1343 var li = typeof laneIdx === "number" && laneIdx >= 0 ? laneIdx : 0; 1344 showCenterJudgment("miss"); 1345 spawnHitBurst(li, "miss"); 1346 updatePlayHud(); 1347 return false; 1348 } 1349 1350 function setTimeLeft(sec) { 1351 if (els.playTime) els.playTime.textContent = String(Math.max(0, Math.ceil(sec))); 1352 } 1353 1354 function refreshComboDock() { 1355 if (!els.comboDock || !els.comboBig) return; 1356 if (state.combo >= 2) { 1357 els.comboDock.classList.remove("is-hidden"); 1358 els.comboBig.textContent = String(state.combo); 1359 els.comboDock.classList.remove("is-pop"); 1360 void els.comboDock.offsetWidth; 1361 els.comboDock.classList.add("is-pop"); 1362 } else { 1363 els.comboDock.classList.add("is-hidden"); 1364 } 1365 } 1366 1367 function applyJudgment(note, j, meta) { 1368 meta = meta || {}; 1369 var laneIdx = typeof note.laneIndex === "number" ? note.laneIndex : 0; 1370 var scoreMult = difficultyScoreMultiplier() * levelScoreMultiplier(); 1371 if (j === "perfect") { 1372 state.perfect += 1; 1373 recordLevelInput(true); 1374 state.combo += 1; 1375 state.score += Math.round((SCORE_PERFECT + state.combo * COMBO_BONUS) * scoreMult); 1376 state.maxCombo = Math.max(state.maxCombo, state.combo); 1377 Sound.hitPopPerfect(); 1378 note.judged = true; 1379 showCenterJudgment(j); 1380 spawnHitBurst(laneIdx, j); 1381 if (note.el && note.el.parentNode) note.el.remove(); 1382 updatePlayHud(); 1383 } else if (j === "good") { 1384 state.good += 1; 1385 recordLevelInput(true); 1386 state.combo += 1; 1387 state.score += Math.round((SCORE_GOOD + Math.floor(state.combo * COMBO_BONUS * 0.5)) * scoreMult); 1388 state.maxCombo = Math.max(state.maxCombo, state.combo); 1389 Sound.hitPopGood(); 1390 note.judged = true; 1391 showCenterJudgment(j); 1392 spawnHitBurst(laneIdx, j); 1393 if (note.el && note.el.parentNode) note.el.remove(); 1394 updatePlayHud(); 1395 } else { 1396 note.judged = true; 1397 if (note.el && note.el.parentNode) note.el.remove(); 1398 penalizeMiss(laneIdx, { source: meta.source || "pass" }); 1399 } 1400 } 1401 1402 function ratioForNote(note, nowPerf) { 1403 if (note.progress != null) return note.progress; 1404 var tr = 1405 note.travelMsForNote != null && note.travelMsForNote > 0 ? note.travelMsForNote : currentTravelMs(); 1406 var spawn = note.spawnPerf != null ? note.spawnPerf : note.hitTime - tr; 1407 return (nowPerf - spawn) / tr; 1408 } 1409 1410 /* -------------------------------------------------------------------------- */ 1411 /* Lanes (DOM) */ 1412 /* -------------------------------------------------------------------------- */ 1413 1414 function relayoutLaneGrid() { 1415 if (!els.lanesBg || !state.keys.length) return; 1416 var n = state.keys.length; 1417 var mobile = window.matchMedia("(max-width: 900px)").matches; 1418 var tpl = mobile 1419 ? "repeat(" + n + ", minmax(38px, 1fr))" 1420 : "repeat(" + n + ", minmax(0, 1fr))"; 1421 els.lanesBg.style.gridTemplateColumns = tpl; 1422 if (els.laneLabels) els.laneLabels.style.gridTemplateColumns = tpl; 1423 refreshActiveNoteAnchors("relayoutLaneGrid"); 1424 } 1425 1426 function buildLaneDom() { 1427 if (!els.lanesBg || !els.laneLabels || !els.notesLayer || !els.lane) return; 1428 var keys = state.keys; 1429 var n = keys.length; 1430 els.lane.classList.add("kb-lane--multi"); 1431 els.lane.style.setProperty("--kb-n-lanes", String(n)); 1432 els.lanesBg.innerHTML = ""; 1433 els.laneLabels.innerHTML = ""; 1434 els.laneColBgs = []; 1435 els.lanePressTimeouts = []; 1436 for (var i = 0; i < n; i++) { 1437 var bg = document.createElement("div"); 1438 bg.className = "kb-lane-col-bg"; 1439 bg.dataset.key = keys[i]; 1440 bg.dataset.laneIndex = String(i); 1441 els.lanesBg.appendChild(bg); 1442 els.laneColBgs.push(bg); 1443 var lab = document.createElement("div"); 1444 lab.className = "kb-lane__label-cell"; 1445 lab.textContent = keys[i]; 1446 lab.dataset.key = keys[i]; 1447 els.laneLabels.appendChild(lab); 1448 } 1449 relayoutLaneGrid(); 1450 if (state.phase === "playing") { 1451 refreshActiveNoteAnchors("buildLaneDom"); 1452 queueBeatPlayfieldLayoutSync(true, "buildLaneDom"); 1453 } 1454 } 1455 1456 function setLanePressedVisual(idx, on) { 1457 if (idx < 0 || !els.laneColBgs || !els.laneColBgs[idx]) return; 1458 els.laneColBgs[idx].classList.toggle("is-pressed", !!on); 1459 var label = els.laneLabels && els.laneLabels.children[idx]; 1460 if (label) label.classList.toggle("is-pressed", !!on); 1461 } 1462 1463 function pulseLanePressed(char) { 1464 var idx = state.keys.indexOf(char); 1465 if (idx < 0) return;
1466 setLanePressedVisual(idx, true); 1467 if (!els.lanePressTimeouts) els.lanePressTimeouts = []; 1468 if (els.lanePressTimeouts[idx]) clearTimeout(els.lanePressTimeouts[idx]); 1469 els.lanePressTimeouts[idx] = setTimeout(function () { 1470 setLanePressedVisual(idx, false); 1471 }, 140); 1472 } 1473 1474 function flashLaneWarn(char) { 1475 var idx = state.keys.indexOf(char); 1476 if (idx < 0 || !els.laneColBgs || !els.laneColBgs[idx]) return; 1477 var el = els.laneColBgs[idx]; 1478 el.classList.add("is-warn"); 1479 setTimeout(function () { 1480 el.classList.remove("is-warn"); 1481 }, 200); 1482 boardShakeWrong(); 1483 } 1484 1485 function boardFlashMiss() { 1486 if (!els.board) return; 1487 els.board.classList.remove("kb-board--miss-shake"); 1488 void els.board.offsetWidth; 1489 els.board.classList.add("kb-board--miss-shake"); 1490 setTimeout(function () { 1491 if (els.board) els.board.classList.remove("kb-board--miss-shake"); 1492 }, 480); 1493 } 1494 1495 function boardShakeWrong() { 1496 if (!els.board) return; 1497 els.board.classList.add("kb-board--shake-soft"); 1498 setTimeout(function () { 1499 if (els.board) els.board.classList.remove("kb-board--shake-soft"); 1500 }, 220); 1501 } 1502 1503 function showCenterJudgment(j) { 1504 if (!els.boardFx) return; 1505 var el = document.createElement("div"); 1506 el.className = "kb-center-judge kb-center-judge--" + j; 1507 el.setAttribute("role", "status"); 1508 el.textContent = j === "perfect" ? "PERFECT" : j === "good" ? "GOOD" : "MISS"; 1509 els.boardFx.appendChild(el); 1510 requestAnimationFrame(function () { 1511 el.classList.add("is-on"); 1512 }); 1513 setTimeout(function () { 1514 el.classList.add("is-out"); 1515 setTimeout(function () { 1516 if (el.parentNode) el.remove(); 1517 }, 480); 1518 }, 420); 1519 } 1520 1521 function spawnHitBurst(laneIndex, j) { 1522 if (!els.boardFx || !state.keys.length) return; 1523 var el = document.createElement("div"); 1524 el.className = "kb-hit-burst kb-hit-burst--" + j; 1525 var leftPct = ((laneIndex + 0.5) / state.keys.length) * 100; 1526 el.style.left = leftPct + "%"; 1527 els.boardFx.appendChild(el); 1528 requestAnimationFrame(function () { 1529 el.classList.add("is-on"); 1530 }); 1531 setTimeout(function () { 1532 if (el.parentNode) el.remove(); 1533 }, j === "perfect" ? 650 : j === "good" ? 520 : 480); 1534 } 1535 1536 /* -------------------------------------------------------------------------- */ 1537 /* Note spawn & DOM */ 1538 /* -------------------------------------------------------------------------- */ 1539 1540 function pickNextLevelNoteKey() { 1541 var lc = state.levelConfig || getLevelConfig(state.currentLevel); 1542 if (!state.patternCtx) state.patternCtx = createPatternContext(lc); 1543 var randRatio = lc.randomRatio != null ? lc.randomRatio : 0.5; 1544 var idx = state.spawnSeqIndex; 1545 state.spawnSeqIndex += 1; 1546 var k = 1547 Math.random() < randRatio ? pickRandomKeyFrom(lc.keys) : pickPatternKey(lc, state.patternCtx, idx); 1548 return ensureScheduleKey(k, state.keys); 1549 } 1550 1551 function spawnNoteAt(nowPerf, key) { 1552 var noteKey = canonicalBeatLaneKey(key) || ensureScheduleKey(key, state.keys); 1553 var travel = currentTravelMs(); 1554 state.idSeq += 1; 1555 var id = "kbn" + state.idSeq; 1556 var laneIndex = laneIndexForKey(noteKey); 1557 if (laneIndex < 0) laneIndex = 0; 1558 var nKeys = state.keys.length || 1; 1559 var leftPct = ((laneIndex + 0.5) / nKeys) * 100; 1560 var el = document.createElement("div"); 1561 el.className = "kb-note"; 1562 el.dataset.id = id; 1563 el.dataset.laneIndex = String(laneIndex); 1564 el.style.left = leftPct + "%"; 1565 el.innerHTML = '<span class="kb-note__char"></span>'; 1566 var ch = el.querySelector(".kb-note__char"); 1567 ch.textContent = noteKey; 1568 if (els.notesLayer) els.notesLayer.appendChild(el); 1569 else if (els.lane && els.hitLine) els.lane.insertBefore(el, els.hitLine); 1570 var noteObj = { 1571 id: id, 1572 key: noteKey, 1573 hitTime: nowPerf + travel, 1574 el: el, 1575 judged: false, 1576 laneIndex: laneIndex, 1577 travelMsForNote: travel, 1578 spawnPerf: nowPerf, 1579 progress: 0, 1580 spawnCenterY: null, 1581 hitCenterY: null, 1582 }; 1583 applyBeatNoteTravelAnchors(noteObj); 1584 state.notes.push(noteObj); 1585 positionBeatNoteElement(noteObj); 1586 } 1587 1588 function tryLevelSpawn(nowPerf) { 1589 var gp = getCurrentBeatGameplayConfig(); 1590 var interval = gp.spawnInterval > 0 ? gp.spawnInterval : 1600; 1591 while (nowPerf >= state.levelNextSpawnPerf && state.phase === "playing") { 1592 spawnNoteAt(nowPerf, pickNextLevelNoteKey()); 1593 state.levelNextSpawnPerf += interval; 1594 if (state.levelNextSpawnPerf > state.levelEndPerf) break; 1595 } 1596 } 1597 1598 function advanceNotes(deltaMs) { 1599 var dt = deltaMs > 0 ? deltaMs : 0; 1600 for (var i = 0; i < state.notes.length; i++) { 1601 var n = state.notes[i]; 1602 if (n.judged || !n.el) continue; 1603 var tr = n.travelMsForNote > 0 ? n.travelMsForNote : currentTravelMs(); 1604 n.progress = (n.progress != null ? n.progress : 0) + dt / tr; 1605 positionBeatNoteElement(n); 1606 } 1607 } 1608 1609 function clearPlayfieldNotes() { 1610 if (els.notesLayer) els.notesLayer.querySelectorAll(".kb-note").forEach(function (x) { x.remove(); }); 1611 else if (els.lane) els.lane.querySelectorAll(".kb-note").forEach(function (x) { x.remove(); }); 1612 state.notes = []; 1613 } 1614 1615 function cullMissed(nowPerf) { 1616 for (var i = state.notes.length - 1; i >= 0; i--) { 1617 var n = state.notes[i]; 1618 if (n.judged) { 1619 state.notes.splice(i, 1); 1620 continue; 1621 } 1622 if (notePassedBeatMissLine(n)) { 1623 logKbMissEvent("pass", n); 1624 applyJudgment(n, "miss", { source: "pass" }); 1625 state.notes.splice(i, 1); 1626 if (state.phase !== "playing") return; 1627 } 1628 } 1629 } 1630 1631 /* -------------------------------------------------------------------------- */ 1632 /* Input */ 1633 /* -------------------------------------------------------------------------- */ 1634 1635 function eventToChar(ev) { 1636 if (!ev || ev.ctrlKey || ev.metaKey || ev.altKey) return ""; 1637 var code = String(ev.code || ""); 1638 if (code.indexOf("Key") === 0 && code.length === 4) { 1639 return code.slice(3).toUpperCase(); 1640 } 1641 if (code === "Semicolon") return ";"; 1642 if (code === "Comma") return ","; 1643 if (code === "Period") return "."; 1644 if (code === "Slash") return "/"; 1645 if (code.indexOf("Digit") === 0 && code.length === 6) { 1646 return code.slice(5); 1647 } 1648 var k = String(ev.key || ""); 1649 if (k === " ") return ""; 1650 if (k.length !== 1) return ""; 1651 return normalizeBeatKeyCompareUnit(k); 1652 } 1653 1654 /** ë¨ì¼ 문ì: ë¼í´ ìíë²³ë§ ë문ìë¡ íµì¼í´ ë¹êµ. ì«ì·ì¸ë¯¸ì½ë¡ ·기í 기í¸ë ê·¸ëë¡ ì ì§ */ 1655 function normalizeBeatKeyCompareUnit(ch) { 1656 if (ch == null || ch === "") return ""; 1657 var s = String(ch); 1658 if (s.length !== 1) return s; 1659 var c = s.charAt(0); 1660 if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z")) return c.toUpperCase(); 1661 return s; 1662 } 1663 1664 function beatKeysEquivalentForJudgment(noteKey, inputKey) { 1665 return normalizeBeatKeyCompareUnit(noteKey) === normalizeBeatKeyCompareUnit(inputKey); 1666 } 1667 1668 /** í¤ë³´ë ì ë ¥(ëë í°ì¹ ë¼ë²¨) â state.keys ë°°ì´ê³¼ ëì¼í 문ìì´(ë ì¸ ì¸ë±ì¤Â·ì¤í° ë ¸í¸ í기ì ëì¼) */ 1669 function canonicalBeatLaneKey(raw) { 1670 return canonicalBeatKeyFromAllow(raw, state.keys); 1671 } 1672 1673 function handlePlayerInput(char, meta) { 1674 if (state.phase !== "playing") return; 1675 var chRaw = char != null ? String(char) : ""; 1676 var canon = canonicalBeatLaneKey(chRaw); 1677 if (!canon) return; 1678 if (meta && meta.source === "touch") { 1679 state.inputMode = "touch"; 1680 } 1681 pulseLanePressed(canon); 1682 tryHitWithChar(canon); 1683 } 1684 1685 function tryHitWithChar(char) { 1686 if (state.phase !== "playing") return; 1687 var want = char; 1688 var best = null; 1689 var bestDist = 9999; 1690 var bestJ = null; 1691 for (var i = 0; i < state.notes.length; i++) { 1692 var n = state.notes[i]; 1693 if (n.judged || !n.el || !beatKeysEquivalentForJudgment(n.key, want)) continue; 1694 var hitJ = isNoteInHitWindow(n); 1695 if (!hitJ) continue; 1696 var dist = Math.abs(getNoteLineDistance(n)); 1697 if (dist < bestDist) { 1698 bestDist = dist; 1699 best = n; 1700 bestJ = hitJ; 1701 } 1702 } 1703 if (!best || !bestJ) { 1704 var laneIx = laneIndexForKey(want); 1705 if (laneIx >= 0) { 1706 flashLaneWarn(want); 1707 penalizeMiss(laneIx, { source: "wrong_key" }); 1708 } 1709 return; 1710 } 1711 applyJudgment(best, bestJ); 1712 for (var j2 = state.notes.length - 1; j2 >= 0; j2--) { 1713 if (state.notes[j2] === best) state.notes.splice(j2, 1); 1714 } 1715 } 1716 1717 function onKeyDown(ev) { 1718 if (state.phase !== "playing") return; 1719 if (ev.repeat) return; 1720 var cRaw = eventToChar(ev); 1721 if (!cRaw) return; 1722 if (!canonicalBeatLaneKey(cRaw)) return; 1723 ev.preventDefault(); 1724 handlePlayerInput(cRaw, { source: "keyboard" }); 1725 } 1726 1727 /* -------------------------------------------------------------------------- */ 1728 /* Animation loop */ 1729 /* -------------------------------------------------------------------------- */ 1730 1731 function showLevelClearOverlay(accuracy, requiredAccuracy, isFinalStage) { 1732 if (!els.kbLevelTransition) return; 1733 var nextLv = state.currentLevel + 1; 1734 if (els.kbLevelTransitionTitle) { 1735 els.kbLevelTransitionTitle.textContent = isFinalStage 1736 ? "LEVEL " + state.currentLevel + " CLEAR!" 1737 : "LEVEL " + state.currentLevel + " CLEAR!"; 1738 } 1739 if (els.kbLevelTransitionSub) { 1740 var sub = "ì íë " + accuracy + "% / 목í " + requiredAccuracy + "%"; 1741 if (isFinalStage) sub += "\nìµì¢ ì¤í ì´ì§ ëí!"; 1742 sub += "\nLEVEL " + nextLv + " ìì"; 1743 els.kbLevelTransitionSub.textContent = sub; 1744 } 1745 els.kbLevelTransition.classList.remove("is-hidden"); 1746 } 1747 1748 function hideLevelClearOverlay() { 1749 if (els.kbLevelTransition) els.kbLevelTransition.classList.add("is-hidden"); 1750 } 1751 1752 function startCampaignLevel(levelNum) { 1753 if (!warnMissingPlayfield("startCampaignLevel")) { 1754 showSetup(); 1755 return; 1756 } 1757 state.currentLevel = levelNum; 1758 state.maxLevelReached = Math.max(state.maxLevelReached, levelNum); 1759 resetLevelCounters(); 1760 clearPlayfieldNotes(); 1761 if (els.boardFx) { 1762 els.boardFx.querySelectorAll(".kb-center-judge, .kb-hit-burst").forEach(function (x) { x.remove(); }); 1763 } 1764 resolveBeatKeysForLevel(levelNum); 1765 applyTravelMsFromGameplayConfig(); 1766 state.patternCtx = createPatternContext(state.levelConfig); 1767 state.spawnSeqIndex = 0; 1768 buildLaneDom(); 1769 var gp = getCurrentBeatGameplayConfig(); 1770 var now = performance.now(); 1771 state.levelStartPerf = now; 1772 state.levelEndPerf = now + LEVEL_DURATION * 1000; 1773 state.levelNextSpawnPerf = now + Math.min(900, gp.spawnInterval * 0.55); 1774 state.lastFramePerf = now; 1775 state.phase = "playing"; 1776 setTimeLeft(LEVEL_DURATION); 1777 updatePlayHud(); 1778 refreshTouchRow(); 1779 syncBeatPlayfieldLayout(false, "startCampaignLevel"); 1780 queueBeatPlayfieldLayoutSync(true, "startCampaignLevel"); 1781 beatDebugLog("level start", { 1782 playAreaHeight: state.beatPf ? state.beatPf.stageHeight : null, 1783 judgementLineTop: state.beatPf ? state.beatPf.lineTop : null, 1784 judgementLineBottom: state.beatPf ? state.beatPf.lineBottom : null,
1785 missLineY: state.beatPf ? state.beatPf.hitCenterY : null, 1786 level: state.currentLevel, 1787 speed: state.speedBias, 1788 keyPool: state.keys.slice(), 1789 travelMs: state.travelMs, 1790 }); 1791 } 1792 1793 function finishLevel() { 1794 if (state.phase !== "playing") return; 1795 state.phase = "level_transition"; 1796 var accuracy = getLevelAccuracy(); 1797 var gp = getCurrentBeatGameplayConfig(); 1798 var required = gp.requiredAccuracy; 1799 1800 if (accuracy >= required) { 1801 clearPlayfieldNotes(); 1802 state.sessionLevelsCleared += 1; 1803 if (state.currentLevel >= state.unlockedLevel && state.currentLevel < 10) { 1804 state.unlockedLevel = Math.max(state.unlockedLevel, state.currentLevel + 1); 1805 BeatProgressStore.setUnlockedLevel(state.unlockedLevel); 1806 } 1807 var isFinalStage = state.currentLevel >= 10; 1808 showLevelClearOverlay(accuracy, required, isFinalStage); 1809 if (state.levelClearTimerId) clearTimeout(state.levelClearTimerId); 1810 state.levelClearTimerId = setTimeout(function () { 1811 state.levelClearTimerId = 0; 1812 hideLevelClearOverlay(); 1813 startNextLevel(); 1814 }, 1500); 1815 } else { 1816 endGame({ 1817 reason: "accuracy_failed", 1818 accuracy: accuracy, 1819 requiredAccuracy: required, 1820 level: state.currentLevel, 1821 score: state.score, 1822 }); 1823 } 1824 } 1825 1826 function startNextLevel() { 1827 startCampaignLevel(state.currentLevel + 1); 1828 queueBeatPlayfieldLayoutSync(true, "startNextLevel"); 1829 requestBeatFrame(); 1830 } 1831 1832 function tickFrame(now) { 1833 if (state.phase !== "playing") return; 1834 if (!hasMinimalPlayfieldDom()) { 1835 console.warn("[Keyboard Beat] tickFrame aborted: missing playfield DOM"); 1836 showSetup(); 1837 return; 1838 } 1839 var ts = typeof now === "number" ? now : performance.now(); 1840 if (!state.lastFramePerf) state.lastFramePerf = ts; 1841 var deltaMs = Math.min(FRAME_DELTA_MS_CAP, Math.max(0, ts - state.lastFramePerf)); 1842 state.lastFramePerf = ts; 1843 1844 updateBeatPlayfieldMetrics(); 1845 1846 tryLevelSpawn(ts); 1847 advanceNotes(deltaMs); 1848 cullMissed(ts); 1849 if (state.phase !== "playing") return; 1850 1851 if (ts >= state.levelEndPerf) { 1852 finishLevel(); 1853 return; 1854 } 1855 1856 setTimeLeft((state.levelEndPerf - ts) / 1000); 1857 updatePlayHud(); 1858 scheduleNextBeatFrame(); 1859 } 1860 1861 /* -------------------------------------------------------------------------- */ 1862 /* Game flow */ 1863 /* -------------------------------------------------------------------------- */ 1864 1865 function readDifficulty() { 1866 var r = els.root && els.root.querySelector('input[name="kbDifficulty"]:checked'); 1867 var v = r && r.value ? r.value : "easy"; 1868 state.bpm = BPM_BY_DIFF[v] || 90; 1869 } 1870 1871 function setKbText(id, value) { 1872 setTextById(id, value); 1873 } 1874 1875 function updateCampaignStartBrief() { 1876 var gp = getBeatGameplayConfig(1); 1877 setKbText("kbCampaignLevel", "LEVEL 1"); 1878 setKbText("kbCampaignGoal", "목í ì íë: " + gp.requiredAccuracy + "%"); 1879 setKbText( 1880 "kbCampaignBrief", 1881 "ë¨ì´ì§ë í¤ë¥¼ ì íí ì ë ¥íì¸ì · ë ë²¨ë¹ " + LEVEL_DURATION + "ì´" 1882 ); 1883 } 1884 1885 function hasMinimalPlayfieldDom() { 1886 return !!(els.game && els.lane && els.lanesBg && els.notesLayer && els.laneLabels); 1887 } 1888 1889 function warnMissingPlayfield(context) { 1890 if (hasMinimalPlayfieldDom()) return true; 1891 console.warn( 1892 "[Keyboard Beat] Playfield DOM incomplete (" + (context || "") + "). Keeping start screen." 1893 ); 1894 return false; 1895 } 1896 1897 function readDifficultySlug() { 1898 var r = els.root && els.root.querySelector('input[name="kbDifficulty"]:checked'); 1899 return r && r.value ? r.value : "easy"; 1900 } 1901 1902 function keyModeLabel(mode) { 1903 var m = { 1904 home: "í", 1905 left: "ì¼ì", 1906 right: "ì¤ë¥¸ì", 1907 numbers: "ì«ì", 1908 symbols: "기í¸", 1909 }; 1910 return m[mode] || mode; 1911 } 1912 1913 function keyModeSummaryLabel(mode) { 1914 var m = { 1915 home: "í í¬ì§ì ", 1916 left: "ì¼ì", 1917 right: "ì¤ë¥¸ì", 1918 numbers: "ì«ì", 1919 symbols: "ì½ë© 기í¸", 1920 }; 1921 return m[mode] || mode; 1922 } 1923 1924 function difficultySummaryName(slug) { 1925 if (slug === "normal") return "Normal"; 1926 if (slug === "hard") return "Hard";
1927 return "Easy"; 1928 } 1929 1930 function formatKeysPreview(keys) { 1931 if (!keys || !keys.length) return ""; 1932 if (keys.length > 20) return "A ~ Z"; 1933 return keys.join(" "); 1934 } 1935 1936 /** í ì¤: Lv · ë 벨 ì 목 · í¤ ëì´ (ì¤ì ·ìì ì¹´ëì©) */ 1937 function formatLevelKeysOneLine(levelNum, cfg) { 1938 var c = cfg || getLevelConfig(levelNum); 1939 return "Lv." + levelNum + " · " + c.title + " · " + formatKeysPreview(c.keys); 1940 } 1941 1942 /** ì§§ê²: Lv · í¤ ëì´ (íë ì´ HUDì©) */ 1943 function formatLevelKeysPlayHud(levelNum, cfg) { 1944 var c = cfg || getLevelConfig(levelNum); 1945 return "Lv." + levelNum + " · " + formatKeysPreview(c.keys); 1946 } 1947 1948 function updateStartLevelCard() { 1949 var cfg = getLevelConfig(state.selectedLevel); 1950 if (els.kbStartLevelOneLine) { 1951 els.kbStartLevelOneLine.textContent = cfg.title + " · " + formatKeysPreview(cfg.keys); 1952 } 1953 if (els.kbStartLevelKeys) { 1954 els.kbStartLevelKeys.textContent = formatKeysPreview(cfg.keys); 1955 } 1956 if (els.kbStartLevelClear) { 1957 var gp1 = getBeatGameplayConfig(1); 1958 els.kbStartLevelClear.textContent = 1959 "ë ë²¨ë¹ " + 1960 LEVEL_DURATION + 1961 "ì´ Â· ì íë " + 1962 gp1.requiredAccuracy + 1963 "% ì´ì í´ë¦¬ì´ · ì ì ëì "; 1964 } 1965 updateCampaignStartBrief(); 1966 if (els.kbSetupSummaryKeys) { 1967 els.kbSetupSummaryKeys.textContent = "í¤: " + formatKeysPreview(cfg.keys); 1968 } 1969 syncKbSettingsKeysBrief(); 1970 } 1971 1972 function syncKbSettingsKeysBrief() { 1973 if (!els.kbSettingsCurrentKeysBrief) return; 1974 var cfg = getLevelConfig(state.selectedLevel); 1975 els.kbSettingsCurrentKeysBrief.textContent = formatLevelKeysOneLine(state.selectedLevel, cfg); 1976 } 1977 1978 function updatePlayHudLevelBrief() { 1979 if (!els.kbPlayLevelBrief) return; 1980 els.kbPlayLevelBrief.textContent = "LEVEL " + state.currentLevel; 1981 } 1982 1983 function syncBeatProgressFromStore() { 1984 state.unlockedLevel = BeatProgressStore.getUnlockedLevel(); 1985 var sel = BeatProgressStore.getSelectedLevel(); 1986 sel = BeatProgressStore._clampLevel(sel); 1987 if (sel > state.unlockedLevel) sel = state.unlockedLevel; 1988 state.selectedLevel = sel; 1989 BeatProgressStore.setSelectedLevel(state.selectedLevel); 1990 resolveBeatKeys(); 1991 } 1992 1993 function syncLevelPicker() { 1994 if (!els.kbLevelPicker) return; 1995 els.kbLevelPicker.innerHTML = ""; 1996 for (var i = 1; i <= 10; i++) { 1997 var cfg = getLevelConfig(i); 1998 var btn = document.createElement("button"); 1999 btn.type = "button"; 2000 btn.className = "kb-level-pick"; 2001 btn.setAttribute("data-beat-level", String(i)); 2002 btn.setAttribute("role", "option"); 2003 btn.setAttribute("aria-selected", i === state.selectedLevel ? "true" : "false"); 2004 btn.innerHTML = 2005 '<span class="kb-level-pick__num">Lv.' + 2006 i + 2007 '</span><span class="kb-level-pick__meta">' + 2008 cfg.title + 2009 "</span>"; 2010 if (i > state.unlockedLevel) { 2011 btn.disabled = true; 2012 btn.classList.add("kb-level-pick--locked"); 2013 btn.setAttribute("aria-disabled", "true"); 2014 } 2015 if (i === state.selectedLevel) btn.classList.add("kb-level-pick--current"); 2016 els.kbLevelPicker.appendChild(btn); 2017 } 2018 } 2019 2020 function applyBeatLevelResultHeadline(cleared) { 2021 var meta = state.endGameMeta; 2022 if (els.kbResultLevelBlock) els.kbResultLevelBlock.classList.remove("is-hidden"); 2023 if (meta && meta.reason === "accuracy_failed") { 2024 if (els.resultOverlay) els.resultOverlay.classList.remove("kb-result--beat-level-clear"); 2025 if (els.resultTitle) els.resultTitle.textContent = "GAME OVER"; 2026 if (els.resultEndReason) { 2027 els.resultEndReason.textContent = 2028 "LEVEL " + 2029 meta.level + 2030 " ì¤í¨ · ì íë " + 2031 meta.accuracy + 2032 "% / 목í " + 2033 meta.requiredAccuracy + 2034 "%"; 2035 } 2036 if (els.kbResultLevelMsg) { 2037 els.kbResultLevelMsg.textContent = 2038 "LEVEL " + meta.level + " · ì íë " + meta.accuracy + "% / 목í " + meta.requiredAccuracy + "%"; 2039 els.kbResultLevelMsg.classList.remove("kb-result-level-msg--success"); 2040 els.kbResultLevelMsg.classList.add("kb-result-level-msg--fail"); 2041 } 2042 return; 2043 } 2044 if (cleared) { 2045 if (els.resultOverlay) els.resultOverlay.classList.add("kb-result--beat-level-clear"); 2046 if (els.resultTitle) els.resultTitle.textContent = "íë ì´ ìë£"; 2047 if (els.resultEndReason) { 2048 els.resultEndReason.textContent = 2049 "ìµê³ LEVEL " + state.maxLevelReached + " · í´ë¦¬ì´ " + state.sessionLevelsCleared + "í"; 2050 } 2051 if (els.kbResultLevelMsg) { 2052 els.kbResultLevelMsg.textContent = "LEVEL " + state.maxLevelReached + "ê¹ì§ ëë¬"; 2053 els.kbResultLevelMsg.classList.remove("kb-result-level-msg--fail"); 2054 els.kbResultLevelMsg.classList.add("kb-result-level-msg--success"); 2055 } 2056 } else { 2057 if (els.resultOverlay) els.resultOverlay.classList.remove("kb-result--beat-level-clear"); 2058 if (els.resultTitle) els.resultTitle.textContent = "GAME OVER"; 2059 if (els.resultEndReason) { 2060 els.resultEndReason.textContent = 2061 "LEVEL " + state.maxLevelReached + " · ìµì¢ ì ì " + state.score; 2062 } 2063 if (els.kbResultLevelMsg) { 2064 els.kbResultLevelMsg.textContent = "ë¤ì ëì í´ë³´ì¸ì."; 2065 els.kbResultLevelMsg.classList.remove("kb-result-level-msg--success"); 2066 els.kbResultLevelMsg.classList.add("kb-result-level-msg--fail"); 2067 } 2068 } 2069 } 2070 2071 function syncBeatResultActionEmphasis(cleared) { 2072 var mains = els.root ? els.root.querySelectorAll(".kb-btn--result-main") : []; 2073 for (var mi = 0; mi < mains.length; mi++) { 2074 mains[mi].classList.remove("kb-btn--result-hero"); 2075 } 2076 if (els.againBtn) els.againBtn.classList.add("kb-btn--result-hero"); 2077 if (els.kbGuestAgainBtn) els.kbGuestAgainBtn.classList.add("kb-btn--result-hero"); 2078 } 2079 2080 function updateResultLevelUi(cleared) { 2081 applyBeatLevelResultHeadline(cleared); 2082 if (els.kbNextLevelBtns && els.kbNextLevelBtns.length) { 2083 for (var bi = 0; bi < els.kbNextLevelBtns.length; bi++) { 2084 els.kbNextLevelBtns[bi].classList.add("is-hidden"); 2085 } 2086 } 2087 if (els.kbGuestAgainBtn) { 2088 els.kbGuestAgainBtn.textContent = "ë¤ì ëì "; 2089 els.kbGuestAgainBtn.classList.toggle("kb-btn--secondary", !!cleared); 2090 els.kbGuestAgainBtn.classList.toggle("kb-btn--primary", !cleared); 2091 } 2092 if (els.againBtn) { 2093 els.againBtn.textContent = "ë¤ì ëì "; 2094 els.againBtn.classList.toggle("kb-btn--secondary", !!cleared); 2095 els.againBtn.classList.toggle("kb-btn--primary", !cleared); 2096 } 2097 syncBeatResultActionEmphasis(cleared); 2098 syncKbResultPrimaryActionsLayout(); 2099 } 2100 2101 /** ë 벨 í´ë¦¬ì´: í° [ëí¹ ë³´ê¸°] ì¨ê¸°ê³ ë³´ì¡° í ì¤í¸ ë§í¬ë§. ì¤í¨/ë¹í´ë¦¬ì´: ê·¸ ë°ë. */ 2102 function syncKbBeatResultFooterLayout(levelClearMode) { 2103 if (els.kbGuestResultRankingBtn) els.kbGuestResultRankingBtn.classList.toggle("is-hidden", !!levelClearMode); 2104 if (els.kbGuestResultRankingLink) els.kbGuestResultRankingLink.classList.toggle("is-hidden", !levelClearMode); 2105 if (els.kbLoggedResultRankingBtn) els.kbLoggedResultRankingBtn.classList.toggle("is-hidden", !!levelClearMode); 2106 if (els.kbLoggedResultRankingLink) els.kbLoggedResultRankingLink.classList.toggle("is-hidden", !levelClearMode); 2107 if (levelClearMode) { 2108 setBeatGuestHomeLinkHidden(true); 2109 } else { 2110 setBeatGuestHomeLinkHidden(false); 2111 } 2112 syncKbResultPrimaryActionsLayout(); 2113 } 2114 2115 function hideResultLevelExtras() { 2116 if (els.kbResultLevelBlock) els.kbResultLevelBlock.classList.add("is-hidden"); 2117 if (els.kbNextLevelBtns && els.kbNextLevelBtns.length) { 2118 for (var bi = 0; bi < els.kbNextLevelBtns.length; bi++) { 2119 els.kbNextLevelBtns[bi].classList.add("is-hidden"); 2120 } 2121 } 2122 } 2123 2124 function goToNextLevelFromResult() { 2125 if (state.selectedLevel >= 10) return; 2126 if (!isKbLoggedIn() && typeof PlayLabGuest !== "undefined" && !PlayLabGuest.canGuestStartAnother("keyboard_beat")) { 2127 PlayLabGuest.showGuestLoginModal({ variant: "daily_limit", game: "keyboard_beat" }); 2128 return; 2129 } 2130 state.selectedLevel = BeatProgressStore._clampLevel(state.selectedLevel + 1); 2131 BeatProgressStore.setSelectedLevel(state.selectedLevel); 2132 resolveBeatKeys(); 2133 if (els.resultOverlay) els.resultOverlay.classList.add("is-hidden"); 2134 hideBeatRankingSectionFull();
2135 kickoffKeyboardBeatRound(); 2136 } 2137 2138 function updateSetupSummary() { 2139 if (!els.kbSetupSummary) return; 2140 var slug = readDifficultySlug(); 2141 var bpm = BPM_BY_DIFF[slug] != null ? BPM_BY_DIFF[slug] : BPM_BY_DIFF.easy; 2142 var cfg = getLevelConfig(state.selectedLevel); 2143 els.kbSetupSummary.textContent = 2144 "Level " + 2145 state.selectedLevel + 2146 " · " + 2147 difficultySummaryName(slug) + 2148 " · " + 2149 bpm + 2150 " BPM"; 2151 if (els.kbSetupSummaryKeys) { 2152 els.kbSetupSummaryKeys.textContent = "í¤: " + formatKeysPreview(cfg.keys); 2153 } 2154 syncKbSettingsKeysBrief(); 2155 } 2156 2157 function closeKbSettingsLayer() { 2158 if (!els.kbSettingsLayer) return; 2159 els.kbSettingsLayer.classList.add("is-hidden"); 2160 els.kbSettingsLayer.setAttribute("aria-hidden", "true"); 2161 document.body.classList.remove("kb-beat-settings-open"); 2162 } 2163 2164 function closeKbBeatPreviewModal() { 2165 if (!els.kbBeatPreviewLayer) return; 2166 els.kbBeatPreviewLayer.classList.add("is-hidden"); 2167 els.kbBeatPreviewLayer.setAttribute("aria-hidden", "true"); 2168 } 2169 2170 function openKbSettingsLayer() { 2171 if (!els.kbSettingsLayer) return; 2172 closeKbRankingLayer(); 2173 closeKbBeatPreviewModal(); 2174 els.kbSettingsLayer.classList.remove("is-hidden"); 2175 els.kbSettingsLayer.setAttribute("aria-hidden", "false"); 2176 document.body.classList.add("kb-beat-settings-open"); 2177 syncLevelPicker(); 2178 updateSetupSummary(); 2179 } 2180 2181 /** 2182 * ëí¹ key_mode ì¬ë¬ê·¸. #kbKeySet UI ì ê±° íìë ë 벨 í¤ ë°°ì´(KEYBOARD_BEAT_LEVELS)ê³¼ í·ê°ë¦¬ì§ ìê² 2183 * ì´ ê°ë§ì¼ë¡ íë ì´ í¤ë¥¼ ì¶ë¡ íì§ ë§ ê². ë 벨/í¤ ë§¤íì 기ë¡íê³ ì¶ë¤ë©´ API íì¥ ê²í . 2184 */ 2185 function readKeySet() { 2186 var v = els.keySet && els.keySet.value ? String(els.keySet.value).trim() : ""; 2187 return KEY_SETS[v] ? v : "home"; 2188 } 2189 2190 /** Keyboard Race ì ëì¼í ì¤ë½ì¤ ëë¤ì ê·ì¹ (í´ë¼ì´ì¸í¸) */ 2191 function validateNicknameClient(name) { 2192 var normalized = normalizeArcadeName(name); 2193 return normalized.length >= 2 && normalized.length <= 10; 2194 } 2195 2196 function normalizeArcadeName(raw) { 2197 return String(raw || "") 2198 .trim() 2199 .replace(/[^A-Za-z0-9ê°-í£]/g, "") 2200 .slice(0, 10); 2201 } 2202 2203 function beatRankingDurationMs() { 2204 if (state.lastDurationMs) return state.lastDurationMs; 2205 if (state.gameStartPerf > 0) { 2206 return Math.max(0, Math.round(performance.now() - state.gameStartPerf)); 2207 } 2208 return 0; 2209 } 2210 2211 function buildBeatRankingPayload(accNum, guestPlayerName) { 2212 var diff = readDifficultySlug(); 2213 var keyMode = readKeySet(); 2214 var lc = getLevelConfig(state.maxLevelReached); 2215 var body = { 2216 score: state.score, 2217 max_combo: state.maxCombo, 2218 accuracy: accNum, 2219 perfect_count: state.perfect, 2220 good_count: state.good, 2221 miss_count: state.miss, 2222 difficulty: diff, 2223 bpm: state.bpm, 2224 key_mode: keyMode, 2225 duration_ms: beatRankingDurationMs(), 2226 input_mode: state.inputMode === "touch" ? "touch" : "keyboard", 2227 level: state.maxLevelReached, 2228 level_label: "LEVEL " + state.maxLevelReached + " · " + lc.title, 2229 level_score_multiplier: lc.scoreMultiplier != null ? lc.scoreMultiplier : 1, 2230 }; 2231 if (guestPlayerName != null && guestPlayerName !== "") { 2232 body.player_name = guestPlayerName; 2233 } 2234 return body; 2235 } 2236 2237 function parseRankingFetchResponse(res) { 2238 return res.text().then(function (txt) { 2239 var data = {}; 2240 try { 2241 data = txt ? JSON.parse(txt) : {}; 2242 } catch (eParse) { 2243 data = { message: txt ? String(txt).slice(0, 200) : "Invalid JSON response" }; 2244 } 2245 return { ok: res.ok, status: res.status, data: data }; 2246 }); 2247 } 2248 2249 function readStoredBeatRankingDisplayName() { 2250 try { 2251 return String(localStorage.getItem(KB_RANKING_DISPLAY_NAME_KEY) || "").trim(); 2252 } catch (eRead) { 2253 return ""; 2254 } 2255 } 2256 2257 function writeStoredBeatRankingDisplayName(name) { 2258 try { 2259 localStorage.setItem(KB_RANKING_DISPLAY_NAME_KEY, String(name || "").trim()); 2260 } catch (eWrite) { 2261 /* ignore */ 2262 } 2263 } 2264 2265 function getActiveBeatResultActionsRow() { 2266 var host = isKbLoggedIn() ? els.kbLoggedResultActions : els.kbGuestResultActions; 2267 return host ? host.querySelector(".kb-result__actions-row--primary") : null; 2268 } 2269 2270 function syncKbResultPrimaryActionsLayout() { 2271 var row = getActiveBeatResultActionsRow(); 2272 if (!row) return; 2273 var visible = row.querySelectorAll(".kb-btn--result-main:not(.is-hidden
2273)"); 2274 row.classList.remove("kb-result__actions-row--count-1", "kb-result__actions-row--count-2"); 2275 if (visible.length === 1) row.classList.add("kb-result__actions-row--count-1"); 2276 else if (visible.length >= 2) row.classList.add("kb-result__actions-row--count-2"); 2277 } 2278 2279 function setBeatResultFooterVisible(visible) { 2280 var actions = isKbLoggedIn() ? els.kbLoggedResultActions : els.kbGuestResultActions; 2281 if (actions) actions.classList.toggle("is-hidden", !visible); 2282 syncKbResultPrimaryActionsLayout(); 2283 } 2284 2285 function setBeatGuestHomeLinkHidden(hidden) { 2286 if (!els.kbGuestToSetupBtn) return; 2287 els.kbGuestToSetupBtn.classList.toggle("is-hidden", !!hidden); 2288 } 2289 2290 function resetBeatRankingSaveForm() { 2291 state.beatRankingSaveState = "idle"; 2292 if (els.kbResultNameInput) { 2293 els.kbResultNameInput.value = ""; 2294 els.kbResultNameInput.disabled = false; 2295 } 2296 if (els.kbResultSaveRankBtn) { 2297 els.kbResultSaveRankBtn.disabled = false; 2298 els.kbResultSaveRankBtn.classList.remove("is-hidden"); 2299 } 2300 if (els.kbResultNameError) { 2301 els.kbResultNameError.textContent = ""; 2302 els.kbResultNameError.classList.add("is-hidden"); 2303 } 2304 if (els.kbResultRankingSavedStatus) els.kbResultRankingSavedStatus.classList.add("is-hidden"); 2305 if (els.kbResultRankingRetryBtn) els.kbResultRankingRetryBtn.classList.add("is-hidden"); 2306 } 2307 2308 function loadBeatResultRankingTop10() { 2309 if (!els.kbResultRankingTableBody || !els.kbResultRankingTableWrap) return; 2310 els.kbResultRankingTableWrap.classList.remove("is-hidden"); 2311 els.kbResultRankingTableBody.innerHTML = '<tr><td colspan="3">ë¶ë¬ì¤ë ì¤â¦</td></tr>'; 2312 fetch(KB_API_LEADERBOARD + "?limit=10", { credentials: "same-origin", headers: { Accept: "application/json" } }) 2313 .then(function (res) { 2314 return res.json(); 2315 }) 2316 .then(function (data) { 2317 if (!els.kbResultRankingTableBody) return; 2318 var rows = data && data.success && data.rankings ? data.rankings.slice(0, 10) : []; 2319 if (!rows.length) { 2320 els.kbResultRankingTableBody.innerHTML = '<tr><td colspan="3">ìì§ ê¸°ë¡ì´ ìì´ì.</td></tr>'; 2321 return; 2322 } 2323 els.kbResultRankingTableBody.innerHTML = rows 2324 .map(function (row) { 2325 var rankNum = Number(row.rank || 0); 2326 var rankText = rankNum === 1 ? "ð¥ 1" : rankNum === 2 ? "ð¥ 2" : rankNum === 3 ? "ð¥ 3" : String(rankNum); 2327 var name = row.display_name || row.player_name || "â"; 2328 return ( 2329 "<tr><td>" + 2330 rankText + 2331 "</td><td>" + 2332 String(name) + 2333 "</td><td>" + 2334 Number(row.score || 0) + 2335 "</td></tr>" 2336 ); 2337 }) 2338 .join(""); 2339 }) 2340 .catch(function () { 2341 if (els.kbResultRankingTableBody) { 2342 els.kbResultRankingTableBody.innerHTML = '<tr><td colspan="3">ëí¹ì ë¶ë¬ì¤ì§ 못íìµëë¤.</td></tr>'; 2343 } 2344 }); 2345 } 2346 2347 function goToBeatStartAfterSave() { 2348 if (els.resultOverlay) { 2349 els.resultOverlay.classList.add("is-hidden"); 2350 els.resultOverlay.classList.remove("kb-result--score-save"); 2351 } 2352 document.body.classList.remove("kb-beat-ranking-open"); 2353 hideBeatRankingSectionFull(); 2354 resetBeatResultOverlayChrome(); 2355 showSetup(); 2356 } 2357 2358 function hideBeatRankingSectionFull() { 2359 if (els.resultOverlay) els.resultOverlay.classList.remove("kb-result--score-save"); 2360 if (!els.kbResultRankingSection) return; 2361 els.kbResultRankingSection.classList.add("is-hidden"); 2362 els.kbResultRankingSection.classList.remove( 2363 "kb-result-ranking--entry", 2364 "kb-result-ranking--saved", 2365 "kb-result-ranking--secondary" 2366 ); 2367 if (els.kbResultRankingSaveForm) els.kbResultRankingSaveForm.classList.remove("is-hidden"); 2368 if (els.kbResultRankNotice) { 2369 els.kbResultRankNotice.textContent = ""; 2370 els.kbResultRankNotice.classList.add("is-hidden"); 2371 } 2372 if (els.resultRankHint) { 2373 els.resultRankHint.textContent = ""; 2374 els.resultRankHint.classList.add("is-hidden"); 2375 } 2376 resetBeatRankingSaveForm(); 2377 } 2378 2379 function fetchBeatRankingPositionAfterResult(accNum, cleared) { 2380 var qs = new URLSearchParams({ 2381 score: String(state.score), 2382 accuracy: String(accNum), 2383 miss_count: String(state.miss), 2384 difficulty: readDifficultySlug(), 2385 level: String(state.maxLevelReached), 2386 input_mode: state.inputMode === "touch" ? "touch" : "keyboard", 2387 }); 2388 return fetch(KB_API_POSITION + "?" + qs.toString(), { 2389 credentials: "same-origin", 2390 headers: { Accept: "application/json" }, 2391 }) 2392 .then(function (res) { 2393 return res.json().catch(function () { 2394 return {}; 2395 }); 2396 }) 2397 .then(function (data) { 2398 if (state.phase !== "ended") return; 2399 if (!els.resultOverlay || els.resultOverlay.classList.contains("is-hidden")) return; 2400 var isTop10 = !!(data && data.success && data.is_top10); 2401 var rankNum = data && data.rank != null ? Number(data.rank) : 0; 2402 if (isTop10) { 2403 showBeatRankingEntryUi(rankNum); 2404 } else { 2405 hideBeatRankingSectionFull(); 2406 if (!cleared && els.resultRankHint) { 2407 els.resultRankHint.textContent = "ìì½ì§ë§ ëí¹ê¶ìë ë¤ì§ 못íì´ì."; 2408 els.resultRankHint.classList.remove("is-hidden"); 2409 } 2410 } 2411 syncKbResultPrimaryActionsLayout(); 2412 }) 2413 .catch(function () { 2414 if (state.phase !== "ended") return; 2415 if (!els.resultOverlay || els.resultOverlay.classList.contains("is-hidden")) return; 2416 hideBeatRankingSectionFull(); 2417 if (!cleared && els.resultRankHint) { 2418 els.resultRankHint.textContent = "ìì를 íì¸íì§ ëª»íì´ì."; 2419 els.resultRankHint.classList.remove("is-hidden"); 2420 } 2421 }); 2422 } 2423 2424 function resetBeatResultOverlayChrome() { 2425 if (els.resultOverlay) { 2426 els.resultOverlay.classList.remove("kb-result--beat-level-clear"); 2427 } 2428 syncKbBeatResultFooterLayout(false); 2429 hideBeatRankingSectionFull(); 2430 setBeatResultFooterVisible(true); 2431 } 2432 2433 function showBeatRankingEntryUi(rankNum) { 2434 if (!els.kbResultRankingSection) return; 2435 if (els.resultOverlay) els.resultOverlay.classList.add("kb-result--score-save"); 2436 resetBeatRankingSaveForm(); 2437 if (els.kbResultSaveRankBtn) els.kbResultSaveRankBtn.textContent = "ì ì ì ì¥"; 2438 loadBeatResultRankingTop10(); 2439 els.kbResultRankingSection.classList.remove("is-hidden", "kb-result-ranking--saved"); 2440 els.kbResultRankingSection.classList.add("kb-result-ranking--entry", "kb-result-ranking--secondary"); 2441 if (els.kbResultRankingHeading) els.kbResultRankingHeading.textContent = "ëí¹ ì§ì "; 2442 if (els.kbResultRankingPrompt) { 2443 els.kbResultRankingPrompt.textContent = 2444 rankNum && rankNum <= 10 2445 ? "TOP " + rankNum + "! ì´ë¦ì ì ë ¥íê³ ê¸°ë¡ì ì ì¥í´ 주ì¸ì." 2446 : "ì´ë¦ì ì ë ¥íê³ ê¸°ë¡ì ì ì¥í´ 주ì¸ì."; 2447 } 2448 if (els.kbResultRankNotice) { 2449 if (rankNum) { 2450 els.kbResultRankNotice.textContent = "ìì ìì: " + rankNum + "ì"; 2451 els.kbResultRankNotice.classList.remove("is-hidden"); 2452 } else { 2453 els.kbResultRankNotice.textContent = ""; 2454 els.kbResultRankNotice.classList.add("is-hidden"); 2455 } 2456 } 2457 if (els.kbResultRankingSaveForm) els.kbResultRankingSaveForm.classList.remove("is-hidden"); 2458 if (els.kbResultNameInput) { 2459 els.kbResultNameInput.value = readStoredBeatRankingDisplayName(); 2460 els.kbResultNameInput.disabled = false;
2461 } 2462 if (els.resultGuestNudge) { 2463 els.resultGuestNudge.classList.toggle("is-hidden", isKbLoggedIn()); 2464 } 2465 syncKbBeatResultFooterLayout(!!state.lastRoundCleared); 2466 setBeatResultFooterVisible(true); 2467 syncBeatResultActionEmphasis(!!state.lastRoundCleared); 2468 } 2469 2470 function showBeatRankingSavedUi(isPb) { 2471 if (!els.kbResultRankingSection) return; 2472 els.kbResultRankingSection.classList.remove("is-hidden", "kb-result-ranking--entry"); 2473 els.kbResultRankingSection.classList.add("kb-result-ranking--saved"); 2474 if (els.kbResultRankingSaveForm) els.kbResultRankingSaveForm.classList.add("is-hidden"); 2475 if (els.kbResultRankingSavedStatus) { 2476 els.kbResultRankingSavedStatus.textContent = "기ë¡ì´ ì ì¥ëììµëë¤"; 2477 els.kbResultRankingSavedStatus.classList.remove("is-hidden"); 2478 } 2479 if (els.kbResultRankingRetryBtn) els.kbResultRankingRetryBtn.classList.add("is-hidden"); 2480 if (els.resultRecordNote) { 2481 if (isPb) { 2482 els.resultRecordNote.textContent = "ì 기ë¡!"; 2483 els.resultRecordNote.classList.remove("is-hidden"); 2484 } else { 2485 els.resultRecordNote.textContent = ""; 2486 els.resultRecordNote.classList.add("is-hidden"); 2487 } 2488 } 2489 if (els.resultGuestNudge) els.resultGuestNudge.classList.add("is-hidden"); 2490 if (els.resultRankHint) { 2491 els.resultRankHint.textContent = "기ë¡ì´ ì ì¥ëììµëë¤."; 2492 els.resultRankHint.classList.remove("is-hidden"); 2493 } 2494 syncKbBeatResultFooterLayout(!!state.lastRoundCleared); 2495 setBeatResultFooterVisible(true); 2496 syncBeatResultActionEmphasis(!!state.lastRoundCleared); 2497 } 2498 2499 function submitBeatRanking(accNum) { 2500 if (state.beatRankingSaveState === "success" || state.beatRankingSaveState === "pending") return; 2501 2502 var raw = els.kbResultNameInput ? els.kbResultNameInput.value : ""; 2503 if (!String(raw || "").trim()) { 2504 if (els.kbResultNameError) { 2505 els.kbResultNameError.textContent = "ì´ë¦ì ì ë ¥í´ì£¼ì¸ì."; 2506 els.kbResultNameError.classList.remove("is-hidden"); 2507 } 2508 return; 2509 } 2510 var name = normalizeArcadeName(raw); 2511 if (!validateNicknameClient(name)) { 2512 if (els.kbResultNameError) { 2513 els.kbResultNameError.textContent = "2~10ì(íê¸/ì문/ì«ì)ë¡ ì ë ¥í´ì£¼ì¸ì."; 2514 els.kbResultNameError.classList.remove("is-hidden"); 2515 } 2516 return; 2517 } 2518 if (els.kbResultNameError) els.kbResultNameError.classList.add("is-hidden"); 2519 2520 state.beatRankingSaveState = "pending"; 2521 if (els.kbResultSaveRankBtn) { 2522 els.kbResultSaveRankBtn.disabled = true; 2523 els.kbResultSaveRankBtn.textContent = "ì ì¥ ì¤..."; 2524 } 2525 if (els.kbResultRankingRetryBtn) els.kbResultRankingRetryBtn.classList.add("is-hidden"); 2526 2527 var body = buildBeatRankingPayload(accNum, name); 2528 fetch(KB_API_RANKING, { 2529 method: "POST", 2530 headers: { "Content-Type": "application/json", Accept: "application/json" }, 2531 credentials: "same-origin", 2532 body: JSON.stringify(body), 2533 }) 2534 .then(parseRankingFetchResponse) 2535 .then(function (out) { 2536 if (out.ok && out.data && out.data.success) { 2537 state.beatRankingSaveState = "success"; 2538 writeStoredBeatRankingDisplayName(name); 2539 if (typeof PlayLabGuest !== "undefined" && PlayLabGuest.clearPendingResult) { 2540 PlayLabGuest.clearPendingResult(); 2541 } 2542 goToBeatStartAfterSave(); 2543 } else { 2544 state.beatRankingSaveState = "failed"; 2545 if (els.kbResultSaveRankBtn) { 2546 els.kbResultSaveRankBtn.disabled = false; 2547 els.kbResultSaveRankBtn.textContent = "ì ì ì ì¥"; 2548 } 2549 if (els.kbResultNameError) { 2550 els.kbResultNameError.textContent = 2551 (out.data && out.data.message) || "ëí¹ ë±ë¡ì ì¤í¨íìµëë¤. ë¤ì ìëí´ì£¼ì¸ì."; 2552 els.kbResultNameError.classList.remove("is-hidden"); 2553 } 2554 if (els.kbResultRankingRetryBtn) els.kbResultRankingRetryBtn.classList.remove("is-hidden"); 2555 console.warn("[keyboard-beat] ranking POST rejected", out.status, out.data); 2556 } 2557 }) 2558 .catch(function (err) { 2559 state.beatRankingSaveState = "failed"; 2560 if (els.kbResultSaveRankBtn) { 2561 els.kbResultSaveRankBtn.disabled = false;
2562 els.kbResultSaveRankBtn.textContent = "ì ì ì ì¥"; 2563 } 2564 if (els.kbResultNameError) { 2565 els.kbResultNameError.textContent = "ëí¹ ë±ë¡ì ì¤í¨íìµëë¤. ë¤ì ìëí´ì£¼ì¸ì."; 2566 els.kbResultNameError.classList.remove("is-hidden"); 2567 } 2568 if (els.kbResultRankingRetryBtn) els.kbResultRankingRetryBtn.classList.remove("is-hidden"); 2569 console.error("[keyboard-beat] ranking POST network error", err); 2570 }); 2571 } 2572 2573 function isBeatGameVisible() { 2574 return !!(els.game && !els.game.classList.contains("is-hidden")); 2575 } 2576 2577 function setBeatPlayingLayout(active) { 2578 var on = !!active && isBeatGameVisible(); 2579 document.body.classList.toggle("kb-beat-playing", on); 2580 if (els.root) els.root.classList.toggle("kb-beat-root--playing", on); 2581 var panel = document.getElementById("playLabPanelKeyboardBeat"); 2582 if (panel) panel.classList.toggle("kb-beat-panel--playing", on); 2583 if (on) { 2584 updateAppHeight(); 2585 requestAnimationFrame(function () { 2586 updateAppHeight(); 2587 relayoutLaneGrid(); 2588 }); 2589 } 2590 } 2591 2592 function showSetup() { 2593 try { 2594 setBeatPlayingLayout(false); 2595 beatChrome.teardown(); 2596 closeKbSettingsLayer(); 2597 closeKbBeatPreviewModal(); 2598 resetState(); 2599 syncBeatProgressFromStore(); 2600 resolveBeatKeys(); 2601 updatePlayHud(); 2602 setTimeLeft(LEVEL_DURATION); 2603 hideResultLevelExtras(); 2604 resetBeatResultOverlayChrome(); 2605 if (els.resultOverlay) els.resultOverlay.classList.add("is-hidden"); 2606 if (els.resultRankHint) { 2607 els.resultRankHint.textContent = ""; 2608 els.resultRankHint.classList.add("is-hidden"); 2609 } 2610 if (els.resultGuestNudge) els.resultGuestNudge.classList.add("is-hidden"); 2611 if (els.game) els.game.classList.add("is-hidden"); 2612 if (els.countdown) els.countdown.classList.add("is-hidden"); 2613 if (els.setup) { 2614 els.setup.classList.remove("is-hidden"); 2615 els.setup.removeAttribute("hidden"); 2616 els.setup.removeAttribute("aria-hidden"); 2617 els.setup.style.removeProperty("display"); 2618 els.setup.style.removeProperty("visibility"); 2619 els.setup.style.removeProperty("opacity"); 2620 els.setup.style.removeProperty("height"); 2621 els.setup.style.removeProperty("max-height"); 2622 } 2623 if (els.kbHeader) els.kbHeader.classList.remove("is-hidden"); 2624 if (els.kbLayout) { 2625 els.kbLayout.classList.remove("is-hidden"); 2626 els.kbLayout.style.removeProperty("display"); 2627 els.kbLayout.style.removeProperty("height"); 2628 } 2629 if (els.root) { 2630 els.root.style.removeProperty("height"); 2631 els.root.style.removeProperty("min-height"); 2632 } 2633 updateStartLevelCard(); 2634 updateSetupSummary(); 2635 updateCampaignStartBrief(); 2636 } catch (errSetup) { 2637 console.error("[Keyboard Beat] showSetup failed", errSetup); 2638 } 2639 } 2640 2641 function showGameUi() { 2642 if (els.resultOverlay) els.resultOverlay.classList.add("is-hidden"); 2643 if (els.setup) { 2644 els.setup.classList.add("is-hidden"); 2645 els.setup.setAttribute("aria-hidden", "true"); 2646 } 2647 if (els.game) els.game.classList.remove("is-hidden"); 2648 setBeatPlayingLayout(true); 2649 } 2650 2651 function isKbLoggedIn() { 2652 return !!(els.root && els.root.getAttribute("data-kb-logged-in") === "1"); 2653 } 2654 2655 function runCountdown(done) { 2656 if (!els.countdown) return done(); 2657 els.countdown.classList.remove("is-hidden"); 2658 var n = COUNTDOWN_SEC; 2659 function step() { 2660 if (n <= 0) { 2661 els.countdown.textContent = ""; 2662 els.countdown.classList.add("is-hidden"); 2663 done(); 2664 return; 2665 } 2666 els.countdown.textContent = String(n); 2667 n -= 1; 2668 setTimeout(step, 1000); 2669 } 2670 step(); 2671 } 2672 2673 function startBeatMetronome() { 2674 clearInterval(state.beatTimerId); 2675 var interval = 60000 / state.bpm; 2676 state.beatTimerId = setInterval(function () { 2677 if (state.phase === "playing") Sound.beatTick(); 2678 }, interval); 2679 } 2680 2681 /** í리뷰 í¤ë³´ëì data-kb-preview-key ìì±ê°ê³¼ ë§¤ì¹ (문ì ë ì´ë¸ íì¤í) */ 2682 function normalizeKbPreviewKeyAttr(raw) { 2683 var s = String(raw != null ? raw : ""); 2684 if (s.length === 1 && s >= "a" && s <= "z") return s.toUpperCase(); 2685 return s; 2686 } 2687 2688 /** ì´ë² í ë ¸í¸ì ì°ì´ë KEYBOARD_BEAT_LEVELS[].keys 를 ê·¸ëë¡ ëì´í´ íì */ 2689 function formatKeysListForBeatPreview(keys) { 2690 if (!keys || !keys.length) return "â"; 2691 return keys.join(" "); 2692 } 2693 2694 /** í리뷰 ìë´ì©: ë 벨 í¤ì ë¼í´ ìíë²³ì´ ìì¼ë©´ ëì문ì ë¬´ê´ ìë´ ë ¸ì¶ */ 2695 function beatPreviewShowsCaseRelaxNote(keys) { 2696 if (!keys || !keys.length) return false;
2697 var i; 2698 for (i = 0; i < keys.length; i++) { 2699 if (/[a-zA-Z]/.test(String(keys[i] || ""))) return true; 2700 } 2701 return false; 2702 } 2703 2704 function renderKbBeatPreviewHighlights(normKeys) { 2705 if (!els.kbBeatPreviewKeyboard) return; 2706 var set = {}; 2707 for (var i = 0; i < normKeys.length; i++) set[normKeys[i]] = true; 2708 var nodes = els.kbBeatPreviewKeyboard.querySelectorAll(".kr-guide-key[data-kb-preview-key]"); 2709 nodes.forEach(function (el) { 2710 var a = el.getAttribute("data-kb-preview-key"); 2711 el.classList.toggle("is-active", !!set[a]); 2712 }); 2713 } 2714 2715 function populateKbBeatPreviewModal() { 2716 var cfg = getLevelConfig(state.selectedLevel); 2717 var keys = cfg.keys || []; 2718 var norm = []; 2719 for (var i = 0; i < keys.length; i++) norm.push(normalizeKbPreviewKeyAttr(keys[i])); 2720 if (els.kbBeatPreviewLevelLine) { 2721 els.kbBeatPreviewLevelLine.textContent = 2722 "LEVEL 1ë¶í° ìì · ë ë²¨ë¹ " + LEVEL_DURATION + "ì´ Â· " + cfg.title; 2723 } 2724 if (els.kbBeatPreviewKeysLine) els.kbBeatPreviewKeysLine.textContent = formatKeysListForBeatPreview(keys); 2725 if (els.kbBeatPreviewCaseHint) { 2726 if (beatPreviewShowsCaseRelaxNote(keys)) { 2727 els.kbBeatPreviewCaseHint.textContent = 2728 "ë문ìë¡ ë³´ì¬ë ê°ì ìíë²³ í¤ë¥¼ ë르면 ì ëµì ëë¤. Shiftë Caps Lockì íìíì§ ììµëë¤."; 2729 els.kbBeatPreviewCaseHint.classList.remove("is-hidden"); 2730 } else { 2731 els.kbBeatPreviewCaseHint.textContent = ""; 2732 els.kbBeatPreviewCaseHint.classList.add("is-hidden"); 2733 } 2734 } 2735 renderKbBeatPreviewHighlights(norm); 2736 } 2737 2738 function openKbBeatPreviewModal() { 2739 if (!els.kbBeatPreviewLayer) { 2740 beginKeyboardBeatRoundAfterPrep(); 2741 return; 2742 } 2743 populateKbBeatPreviewModal(); 2744 els.kbBeatPreviewLayer.classList.remove("is-hidden"); 2745 els.kbBeatPreviewLayer.setAttribute("aria-hidden", "false"); 2746 if (els.kbBeatPreviewStartBtn) els.kbBeatPreviewStartBtn.focus(); 2747 } 2748 2749 function prepareKbBeatRound() { 2750 closeKbSettingsLayer(); 2751 readDifficulty(); 2752 resolveBeatKeys(); 2753 buildLaneDom(); 2754 } 2755 2756 function beginKeyboardBeatRoundAfterPrep() { 2757 if (!warnMissingPlayfield("beginKeyboardBeatRoundAfterPrep")) { 2758 showSetup(); 2759 return; 2760 } 2761 prepareKbBeatRound(); 2762 Sound.init(); 2763 showGameUi(); 2764 beatChrome.enterMobilePrepFromUserGesture(); 2765 Sound.resume().then(function () { 2766 runCountdown(function () { 2767 if (!isKbLoggedIn() && typeof PlayLabGuest !== "undefined") { 2768 PlayLabGuest.incrementGuestStart("keyboard_beat"); 2769 } 2770 state.phase = "playing"; 2771 if (!beatChrome.isMobileKb()) beatChrome.activatePcPlayingChromeOnly(); 2772 beatChrome.retryFullscreenMobile(); 2773 resetRoundStats(); 2774 state.endGameMeta = null; 2775 hideLevelClearOverlay(); 2776 state.gameStartPerf = performance.now(); 2777 if (els.boardFx) els.boardFx.querySelectorAll(".kb-center-judge, .kb-hit-burst").forEach(function (x) { x.remove(); }); 2778 startCampaignLevel(1); 2779 startBeatMetronome(); 2780 requestBeatFrame(); 2781 }); 2782 }); 2783 } 2784 2785 function shouldAutostartBeatFromUrl() { 2786 try { 2787 var params = new URLSearchParams(window.location.search); 2788 var v = params.get("autostart"); 2789 return v === "1" || v === "true" || v === "yes"; 2790 } catch (eAu) { 2791 return false; 2792 } 2793 } 2794 2795 function canStartBeatGuest() { 2796 if (isKbLoggedIn()) return true; 2797 if (typeof PlayLabGuest !== "undefined" && !PlayLabGuest.canGuestStartAnother("keyboard_beat")) { 2798 PlayLabGuest.showGuestLoginModal({ variant: "daily_limit", game: "keyboard_beat" }); 2799 return false; 2800 } 2801 return true; 2802 } 2803 2804 /** ì¸í¸ë¡(ìì) íë©´ë§ íì */ 2805 function showBeatIntro() { 2806 showSetup(); 2807 } 2808 2809 /** ì¸í¸ë¡ ì¨ê¹ â ê²ì ììì¼ë¡ ì í ì¤ë¹ */ 2810 function hideBeatIntro() { 2811 closeKbBeatPreviewModal(); 2812 closeKbSettingsLayer(); 2813 if (els.setup) { 2814 els.setup.classList.add("is-hidden"); 2815 els.setup.setAttribute("aria-hidden", "true"); 2816 } 2817 if (els.kbHeader) els.kbHeader.classList.add("is-hidden"); 2818 } 2819 2820 /** 2821 * Keyboard Beat ê²ì ìì (ì¸í¸ë¡Â·íì´ì§ ì´ë ìì´ íì¬ DOMìì ë°ë¡ ìì) 2822 * @param {object} [opts] 2823 * @param {boolean} [opts.skipPreview] â trueë©´ í¤ í리뷰 ëª¨ë¬ ìëµ 2824 */ 2825 function startKeyboardBeatGame(opts) { 2826 var options = opts || {}; 2827 if (!canStartBeatGuest()) return; 2828 if (!warnMissingPlayfield("startKeyboardBeatGame")) { 2829 showBeatIntro(); 2830 return; 2831 } 2832 hideBeatIntro(); 2833 if (options.skipPreview !== false) { 2834 beginKeyboardBeatRoundAfterPrep(); 2835 return; 2836 } 2837 openKbBeatPreviewModal(); 2838 } 2839 2840 /** í리뷰 ëª¨ë¬ ê²½ì ìì (ì¤ì ·ê°ì´ëì©) */ 2841 function kickoffKeyboardBeatRound() { 2842 if (!canStartBeatGuest()) return; 2843 hideBeatIntro(); 2844 openKbBeatPreviewModal(); 2845 } 2846 2847 function maybeAutostartBeatOnLoad() { 2848 if (!shouldAutostartBeatFromUrl()) return; 2849 if (!isPlayLabGameSolo()) return; 2850 window.setTimeout(function () { 2851 startKeyboardBeatGame({ skipPreview: true }); 2852 }, 0); 2853 } 2854 2855 function endGame(meta) { 2856 if (state.phase === "ended") return; 2857 if (state.phase !== "playing" && state.phase !== "level_transition") return; 2858 hideLevelClearOverlay(); 2859 if (state.levelClearTimerId) { 2860 clearTimeout(state.levelClearTimerId); 2861 state.levelClearTimerId = 0; 2862 } 2863 state.endGameMeta = meta || null; 2864 state.phase = "ended"; 2865 cancelBeatFrame(); 2866 clearInterval(state.beatTimerId); 2867 state.beatTimerId = 0; 2868 var finishAt = performance.now(); 2869 if (!state.endReason) { 2870 state.endReason = meta && meta.reason === "accuracy_failed" ? "accuracy_failed" : "game_over"; 2871 } 2872 2873 state.beatRankingSaveState = "idle"; 2874 if (els.resultOverlay) { 2875 els.resultOverlay.classList.remove("kb-result--beat-level-clear"); 2876 } 2877 hideBeatRankingSectionFull(); 2878 2879 var denom = state.perfect + state.good + state.miss; 2880 var accNum = denom === 0 ? 0 : Math.round(((state.perfect + state.good) / denom) * 100); 2881 state.lastResultAccNum = accNum; 2882 var cleared = false;
2883 state.lastRoundCleared = false; 2884 BeatProgressStore.setSelectedLevel(state.selectedLevel); 2885 var acc = denom === 0 ? "â" : accNum + "%"; 2886 if (els.resultScore) els.resultScore.textContent = String(state.score); 2887 if (els.resultMaxCombo) els.resultMaxCombo.textContent = String(state.maxCombo); 2888 if (els.resultAcc) els.resultAcc.textContent = acc; 2889 if (els.resultPerfect) els.resultPerfect.textContent = String(state.perfect); 2890 if (els.resultGood) els.resultGood.textContent = String(state.good); 2891 if (els.resultMiss) els.resultMiss.textContent = String(state.miss); 2892 var durationMs = 2893 state.gameStartPerf > 0 ? Math.max(0, Math.round(finishAt - state.gameStartPerf)) : 0; 2894 state.lastDurationMs = durationMs; 2895 var durationSec = Math.round(durationMs / 1000); 2896 if (els.resultDuration) els.resultDuration.textContent = durationSec + "ì´"; 2897 2898 if (els.resultRankHint) { 2899 els.resultRankHint.classList.add("is-hidden"); 2900 els.resultRankHint.textContent = ""; 2901 } 2902 if (els.resultGuestNudge) els.resultGuestNudge.classList.add("is-hidden"); 2903 2904 if (els.kbGuestResultActions) els.kbGuestResultActions.classList.toggle("is-hidden", isKbLoggedIn()); 2905 if (els.kbLoggedResultActions) els.kbLoggedResultActions.classList.toggle("is-hidden", !isKbLoggedIn()); 2906 syncKbResultPrimaryActionsLayout(); 2907 2908 if (els.resultOverlay) els.resultOverlay.classList.remove("is-hidden"); 2909 updateResultLevelUi(cleared); 2910 syncKbBeatResultFooterLayout(cleared); 2911 setBeatResultFooterVisible(true); 2912 2913 if (els.resultRecordNote) { 2914 els.resultRecordNote.textContent = ""; 2915 els.resultRecordNote.classList.add("is-hidden"); 2916 } 2917 2918 if (!isKbLoggedIn() && typeof PlayLabGuest !== "undefined") { 2919 var diff = readDifficultySlug(); 2920 var keyMode = readKeySet(); 2921 PlayLabGuest.writePendingResult({ 2922 game_type: "keyboard_beat", 2923 score: state.score, 2924 accuracy: accNum, 2925 max_combo: state.maxCombo, 2926 perfect: state.perfect, 2927 good: state.good, 2928 miss: state.miss, 2929 difficulty: diff, 2930 key_mode: keyMode, 2931 bpm: state.bpm, 2932 duration_ms: durationMs, 2933 duration: durationMs, 2934 created_at: new Date().toISOString(), 2935 level: state.maxLevelReached, 2936 level_label: 2937 "LEVEL " + state.maxLevelReached + " · " + (state.levelConfig || getLevelConfig(state.currentLevel)).title, 2938 }); 2939 } 2940 2941 function finishBeatPlayfieldCleanup() { 2942 if (els.touchRow) { 2943 els.touchRow.classList.add("is-hidden"); 2944 els.touchRow.innerHTML = ""; 2945 } 2946 clearPlayfieldNotes(); 2947 } 2948 2949 fetchBeatRankingPositionAfterResult(accNum, cleared).then(function () { 2950 finishBeatPlayfieldCleanup(); 2951 }); 2952 } 2953 2954 function closeKbRankingLayer() { 2955 if (!els.kbBeatRankingLayer) return; 2956 els.kbBeatRankingLayer.classList.add("is-hidden"); 2957 els.kbBeatRankingLayer.setAttribute("aria-hidden", "true"); 2958 document.body.classList.remove("kb-beat-ranking-open"); 2959 } 2960 2961 function openKbRankingLayer() { 2962 closeKbSettingsLayer(); 2963 closeKbBeatPreviewModal(); 2964 if (!els.kbBeatRankingLayer) return; 2965 els.kbBeatRankingLayer.classList.remove("is-hidden"); 2966 els.kbBeatRankingLayer.setAttribute("aria-hidden", "false"); 2967 document.body.classList.add("kb-beat-ranking-open"); 2968 loadKbLeaderboard(); 2969 } 2970 2971 function formatBeatDate(iso) { 2972 var s = iso && String(iso); 2973 if (!s) return "â"; 2974 return s.length >= 10 ? s.slice(0, 10) : s; 2975 } 2976 2977 function loadKbLeaderboard() { 2978 if (!els.kbBeatRankingLoading || !els.kbBeatRankingEmpty || !els.kbBeatRankingTableWrap || !els.kbBeatRankingBody) 2979 return; 2980 els.kbBeatRankingLoading.classList.remove("is-hidden"); 2981 els.kbBeatRankingEmpty.classList.add("is-hidden"); 2982 els.kbBeatRankingTableWrap.classList.add("is-hidden"); 2983 els.kbBeatRankingBody.innerHTML = ""; 2984 fetch(KB_API_LEADERBOARD + "?limit=50", { credentials: "same-origin" }) 2985 .then(function (res) { 2986 return res.json(); 2987 }) 2988 .then(function (data) { 2989 els.kbBeatRankingLoading.classList.add("is-hidden"); 2990 if (!data || !data.success) { 2991 els.kbBeatRankingEmpty.textContent = "ëí¹ì ë¶ë¬ì¤ì§ 못íìµëë¤."; 2992 els.kbBeatRankingEmpty.classList.remove("is-hidden"); 2993 return; 2994 } 2995 var rows = data.rankings || []; 2996 if (!rows.length) { 2997 els.kbBeatRankingEmpty.classList.remove("is-hidden"); 2998 return; 2999 } 3000 els.kbBeatRankingTableWrap.classList.remove("is-hidden"); 3001 rows.forEach(function (row) { 3002 var tr = document.createElement("tr"); 3003 function td(txt) { 3004 var c = document.createElement("td"); 3005 c.textContent = txt != null ? String(txt) : ""; 3006 tr.appendChild(c); 3007 } 3008 td(row.rank); 3009 td(row.display_name || row.player_name || "â"); 3010 td(row.score); 3011 td(row.max_combo); 3012 td(Math.round(Number(row.accuracy || 0) * 10) / 10 + "%"); 3013 td( 3014 String(row.perfect_count != null ? row.perfect_count : 0) + 3015 " / " + 3016 String(row.good_count != null ? row.good_count : 0) + 3017 " / " + 3018 String(row.miss_count != null ? row.miss_count : 0) 3019 ); 3020 td(String(row.difficulty || "")); 3021 td(String(row.bpm != null ? row.bpm : "")); 3022 td(keyModeLabel(String(row.key_mode || "")));
3023 td(formatBeatDate(row.created_at)); 3024 els.kbBeatRankingBody.appendChild(tr); 3025 }); 3026 }) 3027 .catch(function () { 3028 els.kbBeatRankingLoading.classList.add("is-hidden"); 3029 els.kbBeatRankingEmpty.textContent = "ëí¹ì ë¶ë¬ì¤ì§ 못íìµëë¤."; 3030 els.kbBeatRankingEmpty.classList.remove("is-hidden"); 3031 }); 3032 } 3033 3034 function stopAll() { 3035 setBeatPlayingLayout(false); 3036 beatChrome.teardown(); 3037 hideLevelClearOverlay(); 3038 if (state.levelClearTimerId) { 3039 clearTimeout(state.levelClearTimerId); 3040 state.levelClearTimerId = 0; 3041 } 3042 state.phase = "idle"; 3043 cancelBeatFrame(); 3044 clearInterval(state.beatTimerId); 3045 clearInterval(state.timeLeftId); 3046 state.beatTimerId = 0; 3047 state.timeLeftId = 0; 3048 if (els.notesLayer) els.notesLayer.querySelectorAll(".kb-note").forEach(function (x) { x.remove(); }); 3049 else if (els.lane) els.lane.querySelectorAll(".kb-note").forEach(function (x) { x.remove(); }); 3050 if (els.boardFx) els.boardFx.querySelectorAll(".kb-center-judge, .kb-hit-burst").forEach(function (x) { x.remove(); }); 3051 if (els.resultOverlay) els.resultOverlay.classList.add("is-hidden"); 3052 state.notes = []; 3053 closeKbBeatPreviewModal(); 3054 } 3055 3056 /* -------------------------------------------------------------------------- */ 3057 /* Touch row (mobile) */ 3058 /* -------------------------------------------------------------------------- */ 3059 3060 function refreshTouchRow() { 3061 if (!els.touchRow) return; 3062 var mobile = window.matchMedia("(max-width: 900px)").matches; 3063 if (!mobile || state.phase !== "playing") { 3064 els.touchRow.classList.add("is-hidden"); 3065 els.touchRow.innerHTML = ""; 3066 return; 3067 } 3068 els.touchRow.classList.remove("is-hidden"); 3069 els.touchRow.innerHTML = ""; 3070 var seen = {}; 3071 state.keys.forEach(function (k) { 3072 if (seen[k]) return; 3073 seen[k] = true; 3074 var b = document.createElement("button"); 3075 b.type = "button"; 3076 b.className = "kb-touch"; 3077 b.textContent = k; 3078 b.setAttribute("aria-label", "í¤ " + k); 3079 b.addEventListener("click", function () { 3080 handlePlayerInput(String(k), { source: "touch" }); 3081 }); 3082 els.touchRow.appendChild(b); 3083 }); 3084 if (state.phase === "playing") { 3085 queueBeatPlayfieldLayoutSync(true, "refreshTouchRow"); 3086 } 3087 } 3088 3089 var onResizeBound = function () { 3090 if (state.phase === "playing") { 3091 refreshTouchRow(); 3092 queueBeatPlayfieldLayoutSync(true, "resize"); 3093 } 3094 }; 3095 3096 /* -------------------------------------------------------------------------- */ 3097 /* Init */ 3098 /* -------------------------------------------------------------------------- */ 3099 3100 function cacheEls() { 3101 els.root = document.getElementById("keyboardBeatRoot"); 3102 if (!els.root) return false; 3103 els.setup = document.getElementById("kbSetup"); 3104 els.game = document.getElementById("kbGame"); 3105 els.resultOverlay = document.getElementById("kbResultOverlay"); 3106 els.countdown = document.getElementById("kbCountdown"); 3107 els.gameStack = document.getElementById("kbGameStack"); 3108 els.playHud = document.getElementById("kbPlayHud"); 3109 els.kbPlayAccuracy = document.getElementById("kbPlayAccuracy"); 3110 els.kbPlayTargetAccuracy = document.getElementById("kbPlayTargetAccuracy"); 3111 els.kbLevelTransition = document.getElementById("kbLevelTransition"); 3112 els.kbLevelTransitionTitle = document.getElementById("kbLevelTransitionTitle"); 3113 els.kbLevelTransitionSub = document.getElementById("kbLevelTransitionSub"); 3114 els.playScore = document.getElementById("kbPlayScore"); 3115 els.playTime = document.getElementById("kbPlayTime"); 3116 els.playComboHud = document.getElementById("kbPlayComboHud"); 3117 els.board = document.getElementById("kbBoard"); 3118 els.boardFx = document.getElementById("kbBoardFx"); 3119 els.comboDock = document.getElementById("kbComboDock"); 3120 els.comboBig = document.getElementById("kbComboBig"); 3121 els.lane = document.getElementById("kbLane"); 3122 els.laneStage = document.getElementById("kbLaneStage"); 3123 els.lanesBg = document.getElementById("kbLanesBg"); 3124 els.notesLayer = document.getElementById("kbNotesLayer"); 3125 els.laneLabels = document.getElementById("kbLaneLabels"); 3126 els.hitLine = document.getElementById("kbHitLine"); 3127 els.touchRow = document.getElementById("kbTouchRow"); 3128 els.kbPlayLevelBrief = document.getElementById("kbPlayLevelBrief"); 3129 els.keySet = document.getElementById("kbKeySet"); 3130 els.kbSetupSummary = document.getElementById("kbSetupSummary"); 3131 els.kbSetupSummaryKeys = document.getElementById("kbSetupSummaryKeys"); 3132 els.kbStartLevelOneLine = document.getElementById("kbStartLevelOneLine"); 3133 els.kbStartLevelKeys = document.getElementById("kbStartLevelKeys"); 3134 els.kbStartLevelClear = document.getElementById("kbStartLevelClear"); 3135 els.kbLayout = document.getElementById("kbLayout"); 3136 els.kbHeader = document.querySelector("#keyboardBeatRoot .kb-header"); 3137 els.kbLevelPicker = document.getElementById("kbLevelPicker"); 3138 els.kbResultLevelBlock = document.getElementById("kbResultLevelBlock"); 3139 els.kbResultLevelMsg = document.getElementById("kbResultLevelMsg"); 3140 els.kbOpenSettingsBtn = document.getElementById("kbOpenSettingsBtn"); 3141 els.kbMenuOpenSettingsBtn = document.getElementById("kbMenuOpenSettingsBtn"); 3142 els.kbMenuOpenRankingBtn = document.getElementById("kbMenuOpenRankingBtn"); 3143 els.kbMenuOpenGuideBtn = document.getElementById("kbMenuOpenGuideBtn"); 3144 els.kbSettingsLayer = document.getElementById("kbSettingsLayer"); 3145 els.kbSettingsBackdrop = document.getElementById("kbSettingsBackdrop"); 3146 els.kbSettingsCloseBtn = document.getElementById("kbSettingsCloseBtn"); 3147 els.kbSettingsConfirmBtn = document.getElementById("kbSettingsConfirmBtn"); 3148 els.kbSettingsDismissBtn = document.getElementById("kbSettingsDismissBtn"); 3149 els.kbSettingsCurrentKeysBrief = document.getElementById("kbSettingsCurrentKeysBrief"); 3150 els.kbBeatPreviewLayer = document.getElementById("kbBeatPreviewLayer"); 3151 els.kbBeatPreviewBackdrop = document.getElementById("kbBeatPreviewBackdrop"); 3152 els.kbBeatPreviewLevelLine = document.getElementById("kbBeatPreviewLevelLine"); 3153 els.kbBeatPreviewKeysLine = document.getElementById("kbBeatPreviewKeysLine"); 3154 els.kbBeatPreviewCaseHint = document.getElementById("kbBeatPreviewCaseHint"); 3155 els.kbBeatPreviewKeyboard = document.getElementById("kbBeatPreviewKeyboard"); 3156 els.kbBeatPreviewCloseBtn = document.getElementById("kbBeatPreviewCloseBtn"); 3157 els.kbBeatPreviewStartBtn = document.getElementById("kbBeatPreviewStartBtn"); 3158 els.startBtn = document.getElementById("kbStartBtn"); 3159 els.againBtn = document.getElementById("kbAgainBtn"); 3160 els.resultTitle = document.getElementById("kbResultTitle"); 3161 els.resultScore = document.getElementById("kbResultScore"); 3162 els.resultEndReason = document.getElementById("kbResultEndReason"); 3163 els.resultMaxCombo = document.getElementById("kbResultMaxCombo"); 3164 els.resultAcc = document.getElementById("kbResultAcc"); 3165 els.resultPerfect = document.getElementById("kbResultPerfect"); 3166 els.resultGood = document.getElementById("kbResultGood"); 3167 els.resultMiss = document.getElementById("kbResultMiss"); 3168 els.resultRecordNote = document.getElementById("kbResultRecordNote"); 3169 els.resultDuration = document.getElementById("kbResultDuration"); 3170 els.resultRankHint = document.getElementById("kbResultRankHint"); 3171 els.resultGuestNudge = document.getElementById("kbResultGuestNudge"); 3172 els.kbResultRankingSection = document.getElementById("kbResultRankingSection"); 3173 els.kbResultRankingTableWrap = document.getElementById("kbResultRankingTableWrap"); 3174 els.kbResultRankingTableBody = document.getElementById("kbResultRankingTableBody"); 3175 els.kbResultRankingHeading = document.getElementById("kbResultRankingHeading"); 3176 els.kbResultRankNotice = document.getElementById("kbResultRankNotice"); 3177 els.kbResultRankingSaveForm = document.getElementById("kbResultRankingSaveForm"); 3178 els.kbResultRankingPrompt = document.getElementById("kbResultRankingPrompt"); 3179 els.kbResultNameInput = document.getElementById("kbResultNameInput"); 3180 els.kbResultNameError = document.getElementById("kbResultNameError"); 3181 els.kbResultSaveRankBtn = document.getElementById("kbResultSaveRankBtn"); 3182 els.kbResultRankingSavedStatus = document.getElementById("kbResultRankingSavedStatus"); 3183 els.kbResultRankingRetryBtn = document.getElementById("kbResultRankingRetryBtn"); 3184 els.kbGuestResultActions = document.getElementById("kbGuestResultActions"); 3185 els.kbLoggedResultActions = document.getElementById("kbLoggedResultActions"); 3186 els.kbGuestAgainBtn = document.getElementById("kbGuestAgainBtn"); 3187 els.kbGuestToSetupBtn = document.getElementById("kbGuestToSetupBtn"); 3188 els.kbGuestResultRankingBtn = document.getElementById("kbGuestResultRankingBtn"); 3189 els.kbGuestResultRankingLink = document.getElementById("kbGuestResultRankingLink"); 3190 els.kbLoggedResultRankingBtn = document.getElementById("kbLoggedResultRankingBtn"); 3191 els.kbLoggedResultRankingLink = document.getElementById("kbLoggedResultRankingLink"); 3192 els.kbOpenRankingBtn = document.getElementById("kbOpenRankingBtn"); 3193 els.kbBeatRankingLayer = document.getElementById("kbBeatRankingLayer"); 3194 els.kbBeatRankingBackdrop = document.getElementById("kbBeatRankingBackdrop"); 3195 els.kbBeatRankingCloseBtn = document.getElementById("kbBeatRankingCloseBtn"); 3196 els.kbBeatRankingLoading = document.getElementById("kbBeatRankingLoading"); 3197 els.kbBeatRankingEmpty = document.getElementById("kbBeatRankingEmpty"); 3198 els.kbBeatRankingTableWrap = document.getElementById("kbBeatRankingTableWrap"); 3199 els.kbBeatRankingBody = document.getElementById("kbBeatRankingBody"); 3200 els.kbNextLevelBtns = els.root.querySelectorAll(".js-kb-next-level"); 3201 return true; 3202 } 3203 3204 function isPlayLabGameSolo() { 3205 return typeof PlayLabRoutes !== "undefined" && PlayLabRoutes.isPlayLabGameSolo(); 3206 } 3207 3208 /** DOM ìì ëë§ ì¬ì© â autostart=1 ë¡ ê²ì ì ì© URL ì´ë */ 3209 function navigateToDedicatedBeatPlay() { 3210 var url = "/play-lab/keyboard-beat"; 3211 if (typeof PlayLabRoutes !== "undefined") { 3212 if (PlayLabRoutes.getPlayLabGameContext && PlayLabRoutes.getPlayLabGameContext() === "app") { 3213 url = (PlayLabRoutes.getAppPlayUrl && PlayLabRoutes.getAppPlayUrl("keyboard-beat")) || url; 3214 } else if (PlayLabRoutes.getWebPlayUrl) { 3215 url = PlayLabRoutes.getWebPlayUrl("keyboard-beat") || url; 3216 } 3217 if (PlayLabRoutes.appendUrlQueryParam) { 3218 url = PlayLabRoutes.appendUrlQueryParam(url, "autostart", "1"); 3219 } else { 3220 url += (url.indexOf("?") >= 0 ? "&" : "?") + "autostart=1"; 3221 } 3222 window.location.assign(url); 3223 return; 3224 } 3225 window.location.assign(url + (url.indexOf("?") >= 0 ? "&" : "?") + "autostart=1"); 3226 } 3227 3228 function replayFromResult() { 3229 startKeyboardBeatGame({ skipPreview: true }); 3230 } 3231 3232 function onBeatStartBtnClick(ev) { 3233 if (ev) { 3234 ev.preventDefault(); 3235 ev.stopPropagation(); 3236 } 3237 if (!els.root || !document.getElementById("keyboardBeatRoot")) { 3238 navigateToDedicatedBeatPlay(); 3239 return; 3240 } 3241 startKeyboardBeatGame({ skipPreview: true }); 3242 } 3243 3244 function bindKeyboardBeatEvents() { 3245 var beatAutostart = shouldAutostartBeatFromUrl() && isPlayLabGameSolo(); 3246 try { 3247 syncBeatProgressFromStore(); 3248 } catch (eSync) { 3249 console.warn("[Keyboard Beat] syncBeatProgressFromStore failed", eSync); 3250 } 3251 try { 3252 if (els.startBtn && els.startBtn.getAttribute("data-kb-start-bound") !== "1") { 3253 els.startBtn.setAttribute("data-kb-start-bound", "1"); 3254 bindClick(els.startBtn, onBeatStartBtnClick, "startButton"); 3255 } else if (!els.startBtn) { 3256 console.warn("[Keyboard Beat] start button missing (#kbStartBtn)"); 3257 } 3258 function bindKbBeatPreviewDismiss(el) { 3259 if (!el) return; 3260 el.addEventListener("click", function () { 3261 closeKbBeatPreviewModal(); 3262 }); 3263 } 3264 bindKbBeatPreviewDismiss(els.kbBeatPreviewCloseBtn); 3265 bindKbBeatPreviewDismiss(els.kbBeatPreviewBackdrop); 3266 if (els.kbBeatPreviewStartBtn) { 3267 els.kbBeatPreviewStartBtn.addEventListener("click", function () { 3268 closeKbBeatPreviewModal(); 3269 beginKeyboardBeatRoundAfterPrep(); 3270 }); 3271 } 3272 if (els.againBtn) els.againBtn.addEventListener("click", replayFromResult); 3273 if (els.kbGuestAgainBtn) els.kbGuestAgainBtn.addEventListener("click", replayFromResult); 3274 if (els.kbGuestToSetupBtn) { 3275 els.kbGuestToSetupBtn.addEventListener("click", function () { 3276 showSetup(); 3277 }); 3278 } 3279 if (els.kbResultRankingRetryBtn) { 3280 els.kbResultRankingRetryBtn.addEventListener("click", function () { 3281 submitBeatRanking(state.lastResultAccNum); 3282 }); 3283 } 3284 if (els.kbResultSaveRankBtn) { 3285 els.kbResultSaveRankBtn.addEventListener("click", function () { 3286 submitBeatRanking(state.lastResultAccNum); 3287 }); 3288 } 3289 if (els.kbResultNameInput) { 3290 els.kbResultNameInput.addEventListener("input", function () { 3291 els.kbResultNameInput.value = normalizeArcadeName(els.kbResultNameInput.value); 3292 if ( 3293 els.kbResultNameError && 3294 els.kbResultNameInput.value && 3295 validateNicknameClient(els.kbResultNameInput.value) 3296 ) { 3297 els.kbResultNameError.classList.add("is-hidden"); 3298 els.kbResultNameError.textContent = ""; 3299 } 3300 }); 3301 els.kbResultNameInput.addEventListener("keydown", function (ev) { 3302 if (ev.key === "Enter") { 3303 ev.preventDefault(); 3304 submitBeatRanking(state.lastResultAccNum); 3305 } 3306 }); 3307 } 3308 function bindOpenRanking(el) { 3309 if (!el) return; 3310 el.addEventListener("click", function () { 3311 openKbRankingLayer(); 3312 }); 3313 } 3314 bindOpenRanking(els.kbOpenRankingBtn); 3315 bindOpenRanking(els.kbMenuOpenRankingBtn); 3316 bindOpenRanking(els.kbGuestResultRankingBtn); 3317 bindOpenRanking(els.kbGuestResultRankingLink); 3318 bindOpenRanking(els.kbLoggedResultRankingBtn); 3319 bindOpenRanking(els.kbLoggedResultRankingLink); 3320 if (els.kbOpenSettingsBtn) { 3321 els.kbOpenSettingsBtn.addEventListener("click", function () { 3322 openKbSettingsLayer(); 3323 }); 3324 } 3325 if (els.kbMenuOpenSettingsBtn) { 3326 els.kbMenuOpenSettingsBtn.addEventListener("click", function () { 3327 openKbSettingsLayer(); 3328 }); 3329 } 3330 if (els.kbMenuOpenGuideBtn) { 3331 els.kbMenuOpenGuideBtn.addEventListener("click", function () { 3332 openKbBeatPreviewModal(); 3333 }); 3334 } 3335 function bindCloseKbSettings(el) { 3336 if (!el) return; 3337 el.addEventListener("click", function () { 3338 closeKbSettingsLayer(); 3339 }); 3340 } 3341 bindCloseKbSettings(els.kbSettingsCloseBtn); 3342 bindCloseKbSettings(els.kbSettingsConfirmBtn); 3343 bindCloseKbSettings(els.kbSettingsDismissBtn); 3344 if (els.kbSettingsBackdrop) { 3345 els.kbSettingsBackdrop.addEventListener("click", function () { 3346 closeKbSettingsLayer(); 3347 }); 3348 } 3349 if (els.root) { 3350 els.root.addEventListener("change", function (ev) { 3351 var t = ev.target; 3352 if (!t) return; 3353 if (t.name === "kbDifficulty") updateSetupSummary(); 3354 }); 3355 } 3356 if (els.kbLevelPicker) { 3357 els.kbLevelPicker.addEventListener("click", function (ev) { 3358 var btn = ev.target && ev.target.closest ? ev.target.closest("[data-beat-level]") : null; 3359 if (!btn || btn.disabled) return; 3360 var lvl = parseInt(btn.getAttribute("data-beat-level"), 10); 3361 if (isNaN(lvl) || lvl < 1) return; 3362 if (lvl > state.unlockedLevel) return; 3363 state.selectedLevel = BeatProgressStore._clampLevel(lvl); 3364 BeatProgressStore.setSelectedLevel(state.selectedLevel); 3365 resolveBeatKeys(); 3366 syncLevelPicker(); 3367 updateSetupSummary(); 3368 updateStartLevelCard(); 3369 }); 3370 } 3371 if (els.kbNextLevelBtns && els.kbNextLevelBtns.length) { 3372 for (var nxi = 0; nxi < els.kbNextLevelBtns.length; nxi++) { 3373 els.kbNextLevelBtns[nxi].addEventListener("click", goToNextLevelFromResult); 3374 } 3375 } 3376 if (els.kbBeatRankingCloseBtn)
3376 { 3377 els.kbBeatRankingCloseBtn.addEventListener("click", function () { 3378 closeKbRankingLayer(); 3379 }); 3380 } 3381 if (els.kbBeatRankingBackdrop) { 3382 els.kbBeatRankingBackdrop.addEventListener("click", function () { 3383 closeKbRankingLayer(); 3384 }); 3385 } 3386 document.addEventListener("keydown", function (ev) { 3387 if (ev.key !== "Escape") return; 3388 if (els.kbBeatRankingLayer && !els.kbBeatRankingLayer.classList.contains("is-hidden")) { 3389 closeKbRankingLayer(); 3390 return; 3391 } 3392 if (els.kbBeatPreviewLayer && !els.kbBeatPreviewLayer.classList.contains("is-hidden")) { 3393 closeKbBeatPreviewModal(); 3394 return; 3395 } 3396 if (els.kbSettingsLayer && !els.kbSettingsLayer.classList.contains("is-hidden")) { 3397 closeKbSettingsLayer(); 3398 } 3399 }); 3400 window.addEventListener("keydown", onKeyDown); 3401 window.addEventListener("resize", onResizeBound); 3402 } catch (eBind) { 3403 console.error("[Keyboard Beat] event bind failed", eBind); 3404 } finally { 3405 if (!isBeatPanelActive()) { 3406 parkBeatPanelUi(); 3407 } else if (beatAutostart) { 3408 maybeAutostartBeatOnLoad(); 3409 } else { 3410 try { 3411 showBeatIntro(); 3412 } catch (eIntro) { 3413 console.error("[Keyboard Beat] showBeatIntro failed", eIntro); 3414 } 3415 } 3416 } 3417 if (typeof PlayLabGuest !== "undefined" && typeof PlayLabGuest.tryFlushPendingResults === "function") { 3418 try { 3419 PlayLabGuest.tryFlushPendingResults(); 3420 } catch (ePl) { 3421 /* ignore */ 3422 } 3423 } 3424 } 3425 3426 function initKeyboardBeat() { 3427 logBeatDomSnapshot("init start"); 3428 try { 3429 if (!cacheEls()) { 3430 console.warn("[Keyboard Beat] #keyboardBeatRoot not found â init skipped"); 3431 return; 3432 } 3433 bindKeyboardBeatEvents(); 3434 logBeatDomSnapshot("init complete"); 3435 } catch (errInit) { 3436 console.error("[Keyboard Beat] init failed", errInit);
3437 try { 3438 cacheEls(); 3439 showSetup(); 3440 logBeatDomSnapshot("init recovered"); 3441 } catch (errRecover) { 3442 console.error("[Keyboard Beat] recovery failed", errRecover); 3443 } 3444 } 3445 } 3446 3447 function bootKeyboardBeatPage() { 3448 if (!document.getElementById("keyboardBeatRoot")) { 3449 return; 3450 } 3451 onBeatViewportChange(); 3452 window.addEventListener("resize", onBeatViewportChange); 3453 window.addEventListener("orientationchange", onBeatViewportChange); 3454 if (window.visualViewport) { 3455 window.visualViewport.addEventListener("resize", onBeatViewportChange); 3456 window.visualViewport.addEventListener("scroll", onBeatViewportChange); 3457 } 3458 logBeatDomSnapshot("DOM ready"); 3459 initKeyboardBeat(); 3460 } 3461 3462 function onPlayLabPanelChange(panelId) { 3463 if (!els.root && !document.getElementById("keyboardBeatRoot")) return; 3464 if (!cacheEls() && !document.getElementById("keyboardBeatRoot")) return; 3465 if (panelId !== "keyboard-beat") { 3466 stopAll(); 3467 parkBeatPanelUi(); 3468 return; 3469 } 3470 if (state.phase === "playing" || state.phase === "level_transition") return; 3471 showSetup(); 3472 } 3473 3474 if (document.readyState === "loading") { 3475 document.addEventListener("DOMContentLoaded", bootKeyboardBeatPage); 3476 } else { 3477 bootKeyboardBeatPage(); 3478 } 3479 3480 window.KeyboardBeat = { 3481 onPlayLabPanelChange: onPlayLabPanelChange, 3482 showSetup: showSetup, 3483 showBeatIntro: showBeatIntro, 3484 hideBeatIntro: hideBeatIntro, 3485 startKeyboardBeatGame: startKeyboardBeatGame, 3486 init: initKeyboardBeat, 3487 }; 3488})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.