11// 초대장 정보는 이 객체만 수정하면 전체 화면에 반영됩니다.
22const WEDDING = {
3- groom : "신랑 " ,
4- bride : "신부 " ,
5- heroName : "오늘의 주인공 " ,
6- introDate : "2026. 10. 10 · SAT · 1:00 PM " ,
7- dateText : "2026년 10월 10일 토요일 오후 1시 " ,
8- place : "웨딩빌리지 대성당 · 1층 그랜드홀 " ,
3+ groom : "남궁찬 " ,
4+ bride : "차은서 " ,
5+ heroName : "남궁찬 " ,
6+ introDate : "2027. 05. 08 · SATURDAY " ,
7+ dateText : "2027년 5월 8일 토요일" ,
8+ place : "인천 웨스턴팰리스 " ,
99 message :
1010 "서로의 가장 좋은 친구로,\n오래오래 같은 방향을 바라보며 걷겠습니다.\n귀한 걸음으로 첫 페이지를 축복해 주세요." ,
11- calendarStart : "20261010T040000Z" ,
12- calendarEnd : "20261010T060000Z" ,
11+ calendarStart : "20270508" ,
12+ calendarEnd : "20270509" ,
13+ calendarAllDay : true ,
1314} ;
1415
16+ const AMORIA_BGM_ID = "hCdooZxzISo" ;
17+
1518const QUESTS = [
1619 {
1720 x : 430 ,
1821 icon : "💗" ,
1922 npc : "헤라 & 사랑의 증표 수집가" ,
2023 label : "사랑의 증표" ,
2124 title : "네 개의 마음을 모아 주세요" ,
22- story : "안내자 헤라를 따라 웨딩빌리지에 도착했어요. 약혼반지를 만들려면 네 마을에서 받은 사랑의 증표가 필요합니다. 함께 웃고, 믿고, 기다리고, 아껴 온 네 가지 마음을 꺼내 보세요." ,
25+ story : "안내자 헤라를 따라 아모리아(웨딩빌리지)에 도착했어요. 약혼반지를 만들려면 네 마을에서 받은 사랑의 증표가 필요합니다. 함께 웃고, 믿고, 기다리고, 아껴 온 네 가지 마음을 꺼내 보세요." ,
2326 task : "헤라의 안내로 입장해 추억 · 믿음 · 기다림 · 다정함의 증표 4개 모으기" ,
2427 action : "웨딩빌리지 입장 & 증표 건네기" ,
2528 toast : "사랑의 증표 4개를 모았습니다!" ,
@@ -135,6 +138,7 @@ const dom = {
135138 calendarButton : document . querySelector ( "#calendar-button" ) ,
136139 finalShareButton : document . querySelector ( "#final-share-button" ) ,
137140 confetti : document . querySelector ( "#confetti" ) ,
141+ bgmPlayer : document . querySelector ( "#bgm-player" ) ,
138142} ;
139143
140144const state = {
@@ -152,6 +156,7 @@ const state = {
152156 keys : { left : false , right : false } ,
153157 soundOn : false ,
154158 audio : null ,
159+ bgmFrame : null ,
155160 toastTimer : null ,
156161} ;
157162
@@ -443,15 +448,21 @@ async function shareInvitation() {
443448
444449function saveCalendar ( ) {
445450 const safeTitle = `${ WEDDING . groom } ♥ ${ WEDDING . bride } 결혼식` ;
451+ const startLine = WEDDING . calendarAllDay
452+ ? `DTSTART;VALUE=DATE:${ WEDDING . calendarStart } `
453+ : `DTSTART:${ WEDDING . calendarStart } ` ;
454+ const endLine = WEDDING . calendarAllDay
455+ ? `DTEND;VALUE=DATE:${ WEDDING . calendarEnd } `
456+ : `DTEND:${ WEDDING . calendarEnd } ` ;
446457 const body = [
447458 "BEGIN:VCALENDAR" ,
448459 "VERSION:2.0" ,
449460 "PRODID:-//Wedding Quest//KR" ,
450461 "BEGIN:VEVENT" ,
451462 `UID:wedding-quest-${ WEDDING . calendarStart } @invitation` ,
452463 `DTSTAMP:${ new Date ( ) . toISOString ( ) . replace ( / [ - : ] / g, "" ) . replace ( / \. \d { 3 } / , "" ) } ` ,
453- `DTSTART: ${ WEDDING . calendarStart } ` ,
454- `DTEND: ${ WEDDING . calendarEnd } ` ,
464+ startLine ,
465+ endLine ,
455466 `SUMMARY:${ safeTitle } ` ,
456467 `LOCATION:${ WEDDING . place } ` ,
457468 `DESCRIPTION:${ WEDDING . message . replaceAll ( "\n" , "\\n" ) } ` ,
@@ -481,42 +492,53 @@ function resetGame() {
481492 showToast ( "첫 번째 퀘스트부터 다시 시작합니다!" ) ;
482493}
483494
484- function createMusic ( ) {
495+ function createAudioEngine ( ) {
485496 const AudioContext = window . AudioContext || window . webkitAudioContext ;
486497 if ( ! AudioContext ) return null ;
487498 const context = new AudioContext ( ) ;
488499 const gain = context . createGain ( ) ;
489500 gain . gain . value = 0.035 ;
490501 gain . connect ( context . destination ) ;
502+ return { context, gain } ;
503+ }
504+
505+ function sendBgmCommand ( command ) {
506+ state . bgmFrame ?. contentWindow ?. postMessage (
507+ JSON . stringify ( { event : "command" , func : command , args : [ ] } ) ,
508+ "*" ,
509+ ) ;
510+ }
511+
512+ function startAmoriaBgm ( ) {
513+ if ( state . bgmFrame ) {
514+ sendBgmCommand ( "playVideo" ) ;
515+ return ;
516+ }
491517
492- const notes = [ 261.63 , 329.63 , 392 , 523.25 , 440 , 392 , 329.63 , 293.66 ] ;
493- let step = 0 ;
494- const timer = setInterval ( ( ) => {
495- if ( ! state . soundOn || context . state !== "running" ) return ;
496- const oscillator = context . createOscillator ( ) ;
497- const noteGain = context . createGain ( ) ;
498- oscillator . type = step % 4 === 3 ? "triangle" : "sine" ;
499- oscillator . frequency . value = notes [ step % notes . length ] ;
500- noteGain . gain . setValueAtTime ( 0 , context . currentTime ) ;
501- noteGain . gain . linearRampToValueAtTime ( 0.7 , context . currentTime + 0.02 ) ;
502- noteGain . gain . exponentialRampToValueAtTime ( 0.001 , context . currentTime + 0.42 ) ;
503- oscillator . connect ( noteGain ) ;
504- noteGain . connect ( gain ) ;
505- oscillator . start ( ) ;
506- oscillator . stop ( context . currentTime + 0.44 ) ;
507- step += 1 ;
508- } , 380 ) ;
509-
510- return { context, gain, timer } ;
518+ const frame = document . createElement ( "iframe" ) ;
519+ const origin = encodeURIComponent ( window . location . origin ) ;
520+ frame . title = "MapleStory Amoria BGM" ;
521+ frame . allow = "autoplay; encrypted-media" ;
522+ frame . referrerPolicy = "strict-origin-when-cross-origin" ;
523+ frame . src = `https://www.youtube-nocookie.com/embed/${ AMORIA_BGM_ID } ?autoplay=1&loop=1&playlist=${ AMORIA_BGM_ID } &controls=0&disablekb=1&fs=0&playsinline=1&enablejsapi=1&origin=${ origin } ` ;
524+ frame . addEventListener ( "load" , ( ) => {
525+ if ( state . soundOn ) sendBgmCommand ( "playVideo" ) ;
526+ } ) ;
527+ dom . bgmPlayer . appendChild ( frame ) ;
528+ state . bgmFrame = frame ;
511529}
512530
513531function toggleSound ( forceOn ) {
514532 const next = typeof forceOn === "boolean" ? forceOn : ! state . soundOn ;
515- if ( ! state . audio ) state . audio = createMusic ( ) ;
516- if ( ! state . audio ) return ;
533+ if ( ! state . audio ) state . audio = createAudioEngine ( ) ;
517534 state . soundOn = next ;
518- if ( next ) state . audio . context . resume ( ) ;
519- else state . audio . context . suspend ( ) ;
535+ if ( next ) {
536+ state . audio ?. context . resume ( ) ;
537+ startAmoriaBgm ( ) ;
538+ } else {
539+ state . audio ?. context . suspend ( ) ;
540+ sendBgmCommand ( "pauseVideo" ) ;
541+ }
520542 dom . soundButton . classList . toggle ( "is-on" , state . soundOn ) ;
521543 dom . soundButton . textContent = state . soundOn ? "♫" : "♪" ;
522544 dom . soundButton . setAttribute ( "aria-label" , state . soundOn ? "배경음 끄기" : "배경음 켜기" ) ;
@@ -545,10 +567,11 @@ function playSuccessSound() {
545567
546568function bindControls ( ) {
547569 window . addEventListener ( "keydown" , ( event ) => {
548- if ( [ "ArrowLeft" , "ArrowRight" , "ArrowUp" , "Alt" ] . includes ( event . key ) ) event . preventDefault ( ) ;
570+ const isJumpKey = [ "Space" , "KeyZ" , "AltLeft" , "AltRight" ] . includes ( event . code ) ;
571+ if ( [ "ArrowLeft" , "ArrowRight" , "ArrowUp" ] . includes ( event . key ) || isJumpKey ) event . preventDefault ( ) ;
549572 if ( event . key === "ArrowLeft" ) state . keys . left = true ;
550573 if ( event . key === "ArrowRight" ) state . keys . right = true ;
551- if ( event . key === "Alt" && ! event . repeat ) jump ( ) ;
574+ if ( isJumpKey && ! event . repeat ) jump ( ) ;
552575 if ( event . key === "ArrowUp" && ! event . repeat ) interact ( ) ;
553576 } ) ;
554577
@@ -619,7 +642,7 @@ function init() {
619642 new Promise ( ( resolve ) => {
620643 const image = new Image ( ) ;
621644 image . onload = image . onerror = resolve ;
622- image . src = "assets/wedding-village-panorama .jpg" ;
645+ image . src = "assets/amoria-background-v2 .jpg" ;
623646 } ) ,
624647 new Promise ( ( resolve ) => {
625648 const image = new Image ( ) ;
0 commit comments