Public Source Viewer

비나래아카이브 개발자 포털

실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.

Redacted View
view/hinana/kivotosExp.ejs
공개 가능
1 <!DOCTYPE html>
2 <html lang="ko" xmlns="http://www.w3.org/1999/xhtml">
3 <head>
4 <meta charset="utf-8" />
5 <meta name="color-scheme" content="light dark">
6 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0">
7 <link rel="manifest" href="/manifest.json">
8 <meta name="theme-color" content="#0a0e17">
9 <meta name="apple-mobile-web-app-title" content="비나래 라운지">
10 <meta property="og:image" content="/image/train_hinana.png" />
11 <meta property="og:description" content="키보토스 광역급행철도 — 플랫폼에서 탑승까지" />
12 <meta property="og:url" content="hinana.moe/hinana/kivotosExp" />
13 <meta property="og:title" content="키보토스 광역급행철도" />
14 <title>키보토스 광역급행철도</title>
15
16 <link rel='stylesheet' href='/vendors/bootstrap/css/bootstrap.min.css' />
17 <script src="/vendors/bootstrap/js/bootstrap.min.js"></script>
18 <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css">
19 <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;700&display=swap" rel="stylesheet">
20 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
21 <script src="/js/popup.js"></script>
22
23 <style>
24 :root {
25 --bg-color: #0a0e17;
26 --accent-color: #c5a059;
27 --text-color: #e7e5e4;
28 --platform-gray: #2a2d35;
29 --platform-light: #3a3d47;
30 --safety-yellow: #f5c518;
31 --nozomi-gold: #c5a059;
32 --hikari-blue: #4a90d9;
33 --led-green: #00ff41;
34 --led-bg: #050f05;
35 }
36
37 *, *::before, *::after { box-sizing: border-box; }
38
39 body, html {
40 margin: 0; padding: 0;
41 width: 100%; height: 100%;
42 background-color: var(--bg-color);
43 color: var(--text-color);
44 overflow: hidden;
45 font-family: 'Noto Sans KR', 'Montserrat', sans-serif;
46 user-select: none;
47 }
48
49 /* ============================
50 START OVERLAY
51 ============================ */
52 #startOverlay {
53 position: fixed; inset: 0;
54 background: #0a0e17;
55 z-index: 100;
56 display: flex; flex-direction: column;
57 align-items: center; justify-content: center;
58 gap: 24px;
59 transition: opacity 0.8s ease;
60 }
61 #startOverlay.fade-out {
62 opacity: 0; pointer-events: none;
63 }
64 .start-logo {
65 font-size: 1.1rem;
66 letter-spacing: 6px;
67 color: var(--accent-color);
68 text-transform: uppercase;
69 font-weight: 700;
70 opacity: 0.7;
71 }
72 .start-title {
73 font-size: clamp(1.4rem, 4vw, 2.4rem);
74 font-weight: 700;
75 text-align: center;
76 line-height: 1.4;
77 }
78 .start-title small {
79 display: block;
80 font-size: 0.65em;
81 color: var(--accent-color);
82 letter-spacing: 3px;
83 margin-top: 6px;
84 }
85 .start-desc {
86 font-size: 0.85rem;
87 color: #8a8a9a;
88 text-align: center;
89 max-width: 340px;
90 line-height: 1.7;
91 }
92 .start-btn {
93 margin-top: 8px;
94 padding: 14px 44px;
95 border: 1px solid var(--accent-color);
96 background: transparent;
97 color: var(--accent-color);
98 font-size: 0.9rem;
99 letter-spacing: 3px;
100 cursor: pointer;
101 transition: background 0.3s, color 0.3s;
102 }
103 .start-btn:hover {
104 background: var(--accent-color);
105 color: #0a0e17;
106 }
107 .start-version {
108 position: absolute;
109 bottom: 20px; right: 24px;
110 font-size: 0.65rem;
111 color: #3a3a4a;
112 letter-spacing: 1px;
113 }
114 .kivotos-logo-container {
115 position: fixed;
116 top: 25px; left: 30px;
117 z-index: 200;
118 opacity: 0.8;
119 transition: opacity 0.3s;
120 display: block;
121 }
122 .kivotos-logo-container:hover { opacity: 1; }
123 .kivotos-logo-img {
124 height: 30px; width: auto;
125 filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.8));
126 }
127
128 /* ============================
129 PLATFORM STAGE
130 ============================ */
131 #platformStage {
132 position: fixed; inset: 0;
133 display: none;
134 background: linear-gradient(180deg, #03070f 0%, #0a0e17 40%, #0d1520 65%, #1c2a1c 100%);
135 overflow: hidden;
136 }
137 #platformStage.active { display: block; }
138
139 /* Stars */
140 .stars-layer {
141 position: absolute; top: 0; left: 0; right: 0; height: 55%;
142 pointer-events: none; z-index: 0;
143 }
144 .star {
145 position: absolute;
146 width: 2px; height: 2px;
147 background: #fff;
148 border-radius: 50%;
149 opacity: 0;
150 animation: twinkle linear infinite;
151 }
152 @keyframes twinkle {
153 0%, 100% { opacity: 0; }
154 50% { opacity: 0.8; }
155 }
156
157 /* Platform floor */
158 .platform-floor {
159 position: absolute;
160 bottom: 0; left: 0; right: 0;
161 height: 34%;
162 background: linear-gradient(180deg, #20242e 0%, #2a2d37 40%, #1e2128 100%);
163 z-index: 1;
164 }
165 .platform-floor::before {
166 /* Concrete texture lines */
167 content: '';
168 position: absolute; inset: 0;
169 background: repeating-linear-gradient(
170 90deg,
171 transparent 0px,
172 transparent 79px,
173 rgba(255,255,255,0.03) 79px,
174 rgba(255,255,255,0.03) 80px
175 );
176 }
177 .platform-floor::after {
178 /* Platform edge highlight */
179 content: '';
180 position: absolute;
181 top: 0; left: 0; right: 0;
182 height: 3px;
183 background: rgba(255,255,255,0.12);
184 }
185
186 /* Safety yellow line */
187 .safety-line {
188 position: absolute;
189 bottom: 34%;
190 left: 0; right: 0;
191 height: 10px;
192 background: repeating-linear-gradient(
193 90deg,
194 var(--safety-yellow) 0px,
195 var(--safety-yellow) 30px,
196 transparent 30px,
197 transparent 40px
198 );
199 z-index: 2;
200 box-shadow: 0 0 8px rgba(245,197,24,0.4);
201 }
202
203 /* Platform edge strip */
204 .platform-edge {
205 position: absolute;
206 bottom: calc(34% + 10px);
207 left: 0; right: 0;
208 height: 18px;
209 background: #1a1d26;
210 border-top: 2px solid rgba(255,255,255,0.06);
211 z-index: 2;
212 }
213
214 /* Track area (below platform) */
215 .track-area {
216 position: absolute;
217 bottom: 0; left: 0; right: 0;
218 height: 34%;
219 overflow: hidden;
220 }
221
222 /* CSS Shinkansen Train */
223 .shinkansen {
224 position: absolute;
225 right: -20px;
226 bottom: 34%;
227 z-index: 3;
228 display: flex;
229 align-items: flex-end;
230 transform: translateY(0);
231 }
232 .train-body {
233 position: relative;
234 width: 480px;
235 height: 90px;
236 background: linear-gradient(180deg, #f0f0f2 0%, #e0e0e4 50%, #c8c8cc 100%);
237 border-radius: 0 0 0 8px;
238 }
239 .train-body::before {
240 /* Blue stripe */
241 content: '';
242 position: absolute;
243 top: 28px; left: 0; right: 0;
244 height: 18px;
245 background: linear-gradient(90deg, #1a3a8f 0%, #2255cc 50%, #1a3a8f 100%);
246 }
247 .train-body::after {
248 /* Window row */
249 content: '';
250 position: absolute;
251 top: 12px; left: 20px; right: 0;
252 height: 14px;
253 background: repeating-linear-gradient(
254 90deg,
255 rgba(60,80,120,0.8) 0px,
256 rgba(60,80,120,0.8) 38px,
257 transparent 38px,
258 transparent 52px
259 );
260 border-radius: 2px;
261 }
262 .train-nose {
263 width: 120px;
264 height: 90px;
265 position: relative;
266 flex-shrink: 0;
267 }
268 .train-nose::before {
269 content: '';
270 position: absolute;
271 bottom: 0; right: 0;
272 width: 0; height: 0;
273 border-style: solid;
274 border-width: 0 0 90px 120px;
275 border-color: transparent transparent #e8e8ec transparent;
276 }
277 .train-nose::after {
278 /* Nose stripe */
279 content: '';
280 position: absolute;
281 bottom: 30px; right: 0;
282 width: 0; height: 0;
283 border-style: solid;
284 border-width: 0 0 18px 60px;
285 border-color: transparent transparent #1a3a8f transparent;
286 }
287 .train-bottom {
288 position: absolute;
289 bottom: -12px; left: 0; right: 0;
290 height: 12px;
291 background: #1a1a22;
292 border-radius: 0 0 4px 4px;
293 }
294 .train-door {
295 position: absolute;
296 bottom: 0;
297 width: 28px; height: 55px;
298 background: rgba(0,0,0,0.15);
299 border: 1px solid rgba(0,0,0,0.2);
300 border-bottom: none;
301 border-radius: 2px 2px 0 0;
302 }
303 .train-door:nth-child(1) { left: 60px; }
304 .train-door:nth-child(2) { left: 160px; }
305 .train-door:nth-child(3) { left: 280px; }
306 .train-door:nth-child(4) { left: 390px; }
307
308 /* Departure board */
309 .departure-board {
310 position: absolute;
311 top: 6%;
312 left: 50%;
313 transform: translateX(-50%);
314 z-index: 10;
315 background: var(--led-bg);
316 border: 2px solid #1a2a1a;
317 border-radius: 4px;
318 padding: 14px 28px;
319 min-width: 420px;
320 box-shadow: 0 0 20px rgba(0,255,65,0.15), inset 0 0 30px rgba(0,0,0,0.5);
321 }
322 .board-header {
323 font-family: 'Courier New', monospace;
324 font-size: 0.75rem;
325 color: var(--led-green);
326 letter-spacing: 4px;
327 text-align: center;
328 margin-bottom: 10px;
329 opacity: 0.7;
330 animation: ledFlicker 4s infinite;
331 }
332 .board-title {
333 font-family: 'Courier New', monospace;
334 font-size: 1.05rem;
335 color: var(--led-green);
336 text-align: center;
337 letter-spacing: 2px;
338 font-weight: bold;
339 margin-bottom: 12px;
340 animation: ledFlicker 3s infinite;
341 }
342 .board-row {
343 display: flex;
344 justify-content: space-between;
345 align-items: center;
346 padding: 4px 0;
347 border-top: 1px solid rgba(0,255,65,0.1);
348 gap: 16px;
349 }
350 .board-row:first-of-type { border-top: none; }
351 .board-train {
352 font-family: 'Courier New', monospace;
353 font-size: 0.78rem;
354 color: var(--led-green);
355 min-width: 120px;
356 animation: ledFlicker 5s infinite;
357 }
358 .board-time {
359 font-family: 'Courier New', monospace;
360 font-size: 0.78rem;
361 color: #ffdd44;
362 letter-spacing: 1px;
363 animation: timeFlicker 1s step-end infinite;
364 }
365 .board-track {
366 font-family: 'Courier New', monospace;
367 font-size: 0.78rem;
368 color: var(--led-green);
369 opacity: 0.8;
370 }
371 @keyframes ledFlicker {
372 0%, 97%, 100% { opacity: 1; }
373 98% { opacity: 0.6; }
374 99% { opacity: 1; }
375 99.5% { opacity: 0.4; }
376 }
377 @keyframes timeFlicker {
378 0%, 49% { opacity: 1; }
379 50%, 99% { opacity: 0.6; }
380 }
381
382 /* Roof structure */
383 .platform-roof {
384 position: absolute;
385 top: 14%;
386 left: 0; right: 0;
387 height: 12px;
388 background: #1e2230;
389 z-index: 2;
390 box-shadow: 0 4px 20px rgba(0,0,0,0.6);
391 }
392 .platform-roof::before {
393 content: '';
394 position: absolute;
395 bottom: -60px; left: 0; right: 0;
396 height: 60px;
397 background: linear-gradient(180deg, rgba(30,34,48,0.4) 0%, transparent 100%);
398 }
399 .roof-support {
400 position: absolute;
401 bottom: 0;
402 width: 8px;
403 background: #252838;
404 }
405 .roof-support:nth-child(1) { left: 15%; height: 480px; }
406 .roof-support:nth-child(2) { left: 40%; height: 480px; }
407 .roof-support:nth-child(3) { left: 65%; height: 480px; }
408 .roof-support:nth-child(4) { right: 8%; height: 480px; }
409
410 /* Platform lights */
411 .platform-light {
412 position: absolute;
413 top: 15%;
414 width: 6px; height: 6px;
415 background: #ffe8a0;
416 border-radius: 50%;
417 box-shadow: 0 0 12px 6px rgba(255,232,160,0.3), 0 0 30px 15px rgba(255,232,160,0.1);
418 z-index: 3;
419 }
420 .platform-light:nth-child(1) { left: 15%; }
421 .platform-light:nth-child(2) { left: 40%; }
422 .platform-light:nth-child(3) { left: 65%; }
423
424 /* Signage on roof support */
425 .platform-sign {
426 position: absolute;
427 top: 17%;
428 left: 38%;
429 z-index: 4;
430 background: #1a2238;
431 border: 1px solid rgba(197,160,89,0.3);
432 padding: 4px 12px;
433 font-size: 0.65rem;
434 letter-spacing: 2px;
435 color: var(--accent-color);
436 }
437
438 /* ============================
439 NPC IMAGE COMMON
440 ============================ */
441 .npc-img {
442 height: 17vh;
443 width: auto;
444 pointer-events: none;
445 display: block;
446 }
447
448 /* NPC Progress indicator */
449 .npc-progress {
450 position: absolute;
451 top: 16px; right: 20px;
452 z-index: 20;
453 font-size: 0.72rem;
454 color: var(--accent-color);
455 letter-spacing: 1px;
456 background: rgba(10,14,23,0.7);
457 padding: 6px 14px;
458 border: 1px solid rgba(197,160,89,0.3);
459 border-radius: 2px;
460 }
461
462 /* NPCs */
463 .npc {
464 position: absolute;
465 bottom: 34%;
466 z-index: 8;
467 cursor: pointer;
468 transition: transform 0.15s;
469 display: flex;
470 flex-direction: column;
471 align-items: center;
472 }
473 .npc:hover { transform: scale(1.04) translateY(-3px); }
474 .npc.talked { filter: drop-shadow(0 0 12px rgba(197,160,89,0.7)) drop-shadow(0 0 24px rgba(197,160,89,0.3)); }
475
476 /* NPC 1: 노조미 */
477 #npcNozomi {
478 right: 18%;
479 bottom: 34%;
480 }
481
482 /* NPC 2: 히카리 */
483 #npcHikari {
484 left: 10%;
485 bottom: 34%;
486 }
487
488 /* NPC 3: 아오바 */
489 #npcAoba {
490 left: 40%;
491 bottom: 34%;
492 }
493
494 .npc-label {
495 margin-top: 6px;
496 font-size: 0.65rem;
497 color: rgba(200,200,220,0.6);
498 letter-spacing: 1px;
499 }
500
501 /* Speech bubbles */
502 .speech-bubble {
503 position: absolute;
504 background: #fff;
505 color: #222;
506 padding: 10px 16px;
507 border-radius: 14px;
508 font-size: 0.82rem;
509 font-weight: 500;
510 line-height: 1.5;
511 z-index: 20;
512 pointer-events: none;
513 box-shadow: 0 4px 16px rgba(0,0,0,0.35);
514 white-space: nowrap;
515 max-width: 280px;
516 white-space: normal;
517 text-align: center;
518 opacity: 0;
519 transform: translateY(6px);
520 transition: opacity 0.25s, transform 0.25s;
521 }
522 .speech-bubble.visible {
523 opacity: 1;
524 transform: translateY(0);
525 }
526 .speech-bubble::after {
527 content: '';
528 position: absolute;
529 bottom: -9px;
530 left: 50%;
531 transform: translateX(-50%);
532 border-width: 9px 8px 0;
533 border-style: solid;
534 border-color: #fff transparent transparent;
535 }
536
537 /* Back button */
538 .back-btn {
539 position: absolute;
540 bottom: 20px; left: 20px;
541 z-index: 20;
542 background: transparent;
543 border: 1px solid rgba(197,160,89,0.4);
544 color: rgba(197,160,89,0.7);
545 padding: 8px 20px;
546 font-size: 0.75rem;
547 letter-spacing: 2px;
548 cursor: pointer;
549 transition: all 0.2s;
550 }
551 .back-btn:hover {
552 background: rgba(197,160,89,0.1);
553 color: var(--accent-color);
554 }
555
556 /* Version string */
557 .version-str {
558 position: absolute;
559 bottom: 20px; right: 20px;
560 z-index: 20;
561 font-size: 0.6rem;
562 color: rgba(100,100,120,0.5);
563 letter-spacing: 1px;
564 }
565
566 /* ============================
567 BOARDING STAGE
568 ============================ */
569 #boardingStage {
570 position: fixed; inset: 0;
571 display: none;
572 background: linear-gradient(135deg, #06080f 0%, #0a0e1a 50%, #060d1a 100%);
573 z-index: 50;
574 align-items: center;
575 justify-content: center;
576 flex-direction: column;
577 gap: 40px;
578 opacity: 0;
579 transition: opacity 0.8s ease;
580 }
581 #boardingStage.active {
582 display: flex;
583 }
584 #boardingStage.visible {
585 opacity: 1;
586 }
587
588 .boarding-announcement {
589 font-size: clamp(0.9rem, 2.5vw, 1.2rem);
590 letter-spacing: 4px;
591 color: var(--accent-color);
592 text-align: center;
593 opacity: 0.9;
594 }
595 .boarding-subtitle {
596 font-size: 0.75rem;
597 letter-spacing: 3px;
598 color: rgba(200,200,220,0.5);
599 text-align: center;
600 margin-top: -30px;
601 }
602
603 .train-cards {
604 display: flex;
605 gap: 32px;
606 flex-wrap: wrap;
607 justify-content: center;
608 }
609
610 .train-card {
611 width: 280px;
612 padding: 36px 28px;
613 border: 1px solid rgba(255,255,255,0.08);
614 border-radius: 4px;
615 cursor: pointer;
616 position: relative;
617 overflow: hidden;
618 transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
619 background: rgba(10,14,23,0.6);
620 text-decoration: none;
621 color: inherit;
622 display: block;
623 }
624 .train-card::before {
625 content: '';
626 position: absolute;
627 top: 0; left: 0; right: 0;
628 height: 3px;
629 }
630 .train-card.nozomi::before { background: linear-gradient(90deg, var(--nozomi-gold), #e8c878, var(--nozomi-gold)); }
631 .train-card.hikari::before { background: linear-gradient(90deg, var(--hikari-blue), #82c4ff, var(--hikari-blue)); }
632
633 .train-card:hover {
634 transform: translateY(-6px);
635 }
636 .train-card.nozomi:hover {
637 box-shadow: 0 16px 40px rgba(197,160,89,0.2);
638 border-color: rgba(197,160,89,0.3);
639 }
640 .train-card.hikari:hover {
641 box-shadow: 0 16px 40px rgba(74,144,217,0.2);
642 border-color: rgba(74,144,217,0.3);
643 }
644
645 .card-jp-name {
646 font-size: 2.2rem;
647 font-weight: 700;
648 margin-bottom: 4px;
649 letter-spacing: 2px;
650 }
651 .train-card.nozomi .card-jp-name { color: var(--nozomi-gold); }
652 .train-card.hikari .card-jp-name { color: var(--hikari-blue); }
653
654 .card-kr-name {
655 font-size: 0.8rem;
656 letter-spacing: 3px;
657 color: rgba(200,200,220,0.5);
658 margin-bottom: 20px;
659 }
660
661 .card-divider {
662 height: 1px;
663 background: rgba(255,255,255,0.07);
664 margin-bottom: 20px;
665 }
666
667 .card-desc {
668 font-size: 0.82rem;
669 color: rgba(200,200,220,0.7);
670 line-height: 1.7;
671 margin-bottom: 24px;
672 }
673
674 .card-badge {
675 display: inline-block;
676 padding: 3px 10px;
677 font-size: 0.62rem;
678 letter-spacing: 2px;
679 border-radius: 2px;
680 margin-bottom: 6px;
681 }
682 .train-card.nozomi .card-badge {
683 background: rgba(197,160,89,0.15);
684 color: var(--nozomi-gold);
685 border: 1px solid rgba(197,160,89,0.25);
686 }
687 .train-card.hikari .card-badge {
688 background: rgba(74,144,217,0.15);
689 color: var(--hikari-blue);
690 border: 1px solid rgba(74,144,217,0.25);
691 }
692
693 .card-arrow {
694 position: absolute;
695 bottom: 20px; right: 24px;
696 font-size: 1.1rem;
697 opacity: 0.4;
698 transition: opacity 0.2s, transform 0.2s;
699 }
700 .train-card:hover .card-arrow {
701 opacity: 0.9;
702 transform: translateX(4px);
703 }
704 .train-card.nozomi .card-arrow { color: var(--nozomi-gold); }
705 .train-card.hikari .card-arrow { color: var(--hikari-blue); }
706
707 .boarding-back {
708 background: transparent;
709 border: 1px solid rgba(197,160,89,0.3);
710 color: rgba(197,160,89,0.6);
711 padding: 10px 30px;
712 font-size: 0.75rem;
713 letter-spacing: 3px;
714 cursor: pointer;
715 transition: all 0.2s;
716 }
717 .boarding-back:hover {
718 background: rgba(197,160,89,0.08);
719 color: var(--accent-color);
720 }
721
722 /* Boarding stage decorative line */
723 .boarding-line {
724 width: 80px;
725 height: 1px;
726 background: linear-gradient(90deg, transparent, var(--accent-color), transparent);
727 opacity: 0.4;
728 }
729 </style>
730 </head>
731 <body>
732
733 <!-- 비나래 라운지 로고 -->
734 <a href="/hinana/lounge" class="kivotos-logo-container" title="라운지로 돌아가기">
735 <img src="/image/lounge1.png" alt="비나래 라운지" class="kivotos-logo-img">
736 </a>
737
738 <!-- ============================
739 START OVERLAY
740 ============================ -->
741 <div id="startOverlay">
742 <div class="start-logo">Kivotos Metropolitan Railway</div>
743 <div class="start-title">
744 키보토스 광역급행철도
745 <small>KMR · のぞみ / ひかり</small>
746 </div>
747 <div class="start-desc">
748 플랫폼으로 나가 탑승을 준비하세요.<br>
749 역무원과 승객들에게 말을 걸어보세요.
750 </div>
751 <button class="start-btn" id="startBtn">플랫폼으로 →</button>
752 <div class="start-version">Ver. 6.5.4.0-Kozeki Ui</div>
753 </div>
754
755 <!-- ============================
756 PLATFORM STAGE
757 ============================ -->
758 <div id="platformStage">
759
760 <!-- Stars -->
761 <div class="stars-layer" id="starsLayer"></div>
762
763 <!-- Roof structure -->
764 <div class="platform-roof">
765 <div class="roof-support"></div>
766 <div class="roof-support"></div>
767 <div class="roof-support"></div>
768 <div class="roof-support"></div>
769 </div>
770 <div class="platform-light"></div>
771 <div class="platform-light"></div>
772 <div class="platform-light"></div>
773
774 <!-- Departure board -->
775 <div class="departure-board">
776 <div class="board-header">◆ KIVOTOS METROPOLITAN RAILWAY ◆</div>
777 <div class="board-title">키보토스 광역급행철도</div>
778 <div class="board-row">
779 <span class="board-train">노조미 (のぞみ) 1호</span>
780 <span class="board-time" id="nozomiTime">--:--</span>
781 <span class="board-track">1번 선</span>
782 </div>
783 <div class="board-row">
784 <span class="board-train">히카리 (ひかり) 3호</span>
785 <span class="board-time" id="hikariTime">--:--</span>
786 <span class="board-track">2번 선</span>
787 </div>
788 </div>
789
790 <!-- Platform sign -->
791 <div class="platform-sign">1번 선 · 노조미</div>
792
793 <!-- CSS Shinkansen -->
794 <div class="shinkansen">
795 <div class="train-nose"></div>
796 <div class="train-body">
797 <div class="train-door"></div>
798 <div class="train-door"></div>
799 <div class="train-door"></div>
800 <div class="train-door"></div>
801 <div class="train-bottom"></div>
802 </div>
803 </div>
804
805 <!-- Platform surfaces -->
806 <div class="platform-edge"></div>
807 <div class="safety-line"></div>
808 <div class="platform-floor"></div>
809
810 <!-- NPC Progress -->
811 <div class="npc-progress" id="npcProgress">0 / 3 NPC와 대화 완료</div>
812
813 <!-- NPC 1: 노조미 -->
814 <div class="npc" id="npcNozomi" onclick="talkToNPC('nozomi')">
815 <img src="/image/nozomi.png" alt="노조미" class="npc-img">
816 <div class="npc-label">노조미</div>
817 </div>
818 <div class="speech-bubble" id="bubbleNozomi"></div>
819
820 <!-- NPC 2: 히카리 -->
821 <div class="npc" id="npcHikari" onclick="talkToNPC('hikari')">
822 <img src="/image/hikari.png" alt="히카리" class="npc-img">
823 <div class="npc-label">히카리</div>
824 </div>
825 <div class="speech-bubble" id="bubbleHikari"></div>
826
827 <!-- NPC 3: 아오바 -->
828 <div class="npc" id="npcAoba" onclick="talkToNPC('aoba')">
829 <img src="/image/aoba.png" alt="아오바" class="npc-img">
830 <div class="npc-label">아오바</div>
831 </div>
832 <div class="speech-bubble" id="bubbleAoba"></div>
833
834 <!-- Back to gallery -->
835 <button class="back-btn" onclick="history.back()">← 갤러리</button>
836 <div class="version-str">Ver. 6.5.4.0-Kozeki Ui</div>
837 </div>
838
839 <!-- ============================
840 BOARDING STAGE
841 ============================ -->
842 <div id="boardingStage">
843 <div class="boarding-announcement">탑승 준비가 완료되었습니다.</div>
844 <div class="boarding-subtitle">열차를 선택하여 탑승하세요</div>
845 <div class="boarding-line"></div>
846
847 <div class="train-cards">
848 <!-- 노조미 -->
849 <a href="/hinana/train?type=nozomi" class="train-card nozomi">
850 <div class="card-badge">최고속 특급</div>
851 <div class="card-jp-name">のぞみ</div>
852 <div class="card-kr-name">노조미</div>
853 <div class="card-divider"></div>
854 <div class="card-desc">
855 최고속 특급 · 키보토스 전역<br>
856 가장 빠르게 목적지에 도착합니다.
857 </div>
858 <div class="card-arrow">→</div>
859 </a>
860
861 <!-- 히카리 -->
862 <a href="/hinana/train?type=hikari" class="train-card hikari">
863 <div class="card-badge">경치형 특급</div>
864 <div class="card-jp-name">ひかり</div>
865 <div class="card-kr-name">히카리</div>
866 <div class="card-divider"></div>
867 <div class="card-desc">
868 경치형 특급 · 여유로운 여행<br>
869 창밖 풍경을 감상하며 달립니다.
870 </div>
871 <div class="card-arrow">→</div>
872 </a>
873 </div>
874
875 <div class="boarding-line"></div>
876 <button class="boarding-back" id="boardingBackBtn">← 플랫폼으로 돌아가기</button>
877 </div>
878
879 <script>
880 // ============================
881 // NPC Dialogue Data
882 // ============================
883 const npcs = {
884 nozomi: {
885 element: document.getElementById('npcNozomi'),
886 bubble: document.getElementById('bubbleNozomi'),
887 lines: [
888 "퍄핫☆, 탑승객이네요. 노조미호는 제가 안내해드릴게요.",
889 "노조미는 키보토스 전역을 도중 정차역을 최소화 하여 최고속으로 연결해요. 빠르게 목적지에 도착하고 싶다면 저를 타세요!",
890 "퍄핫☆, 준비됐으면 바로 올라타요. 출발 시간이 됐거든요."
891 ],
892 index: 0,
893 talked: false,
894 bubbleTimer: null
895 },
896 hikari: {
897 element: document.getElementById('npcHikari'),
898 bubble: document.getElementById('bubbleHikari'),
899 lines: [
900 "안녕하세요~~ 히카리에~~~ 탑승하실 건가요~~~?",
901 "히카리는~~~~...여유롭게 창밖 경치를 즐기며 달리는 경치형 특급이에요. 서두르지 않아도 괜찮아요~~~~",
902 "천천히, 하지만 확실하게 목적지로 데려다드릴게요."
903 ],
904 index: 0,
905 talked: false,
906 bubbleTimer: null
907 },
908 aoba: {
909 element: document.getElementById('npcAoba'),
910 bubble: document.getElementById('bubbleAoba'),
911 lines: [
912 "어서오세요, 키보토스 광역급행철도입니다!",
913 "노조미호는 오른쪽, 히카리호는 왼쪽에 서 계신 분들이 담당하세요. 이야기 나눠보셨나요?",
914 "승차권은 열차 탑승 후에도 확인하실 수 있어요. 좋은 여행 되세요!"
915 ],
916 index: 0,
917 talked: false,
918 bubbleTimer: null
919 }
920 };
921
922 let talkedCount = 0;
923 let boardingShown = false;
924
925 function talkToNPC(npcKey) {
926 const npc = npcs[npcKey];
927 if (npc.talked) {
928 // Already talked to - just flash the last line again briefly
929 showBubble(npc, npc.lines[npc.lines.length - 1]);
930 return;
931 }
932
933 const line = npc.lines[npc.index];
934 showBubble(npc, line);
935 npc.index++;
936
937 if (npc.index >= npc.lines.length) {
938 // Finished all dialogue
939 npc.talked = true;
940 npc.element.classList.add('talked');
941 talkedCount++;
942 document.getElementById('npcProgress').textContent = talkedCount + ' / 3 NPC와 대화 완료';
943
944 if (talkedCount >= 3) {
945 setTimeout(checkAllNPCsTalked, 600);
946 }
947 }
948 }
949
950 function showBubble(npc, text) {
951 const bubble = npc.bubble;
952 if (npc.bubbleTimer) clearTimeout(npc.bubbleTimer);
953
954 // Position bubble above the NPC element
955 const elRect = npc.element.getBoundingClientRect();
956 bubble.textContent = text;
957 bubble.style.position = 'fixed';
958 bubble.style.bottom = 'auto';
959 bubble.style.top = (elRect.top - 90) + 'px';
960 bubble.style.left = (elRect.left + elRect.width / 2) + 'px';
961 bubble.style.transform = 'translateX(-50%)';
962
963 bubble.classList.add('visible');
964
965 npc.bubbleTimer = setTimeout(() => {
966 bubble.classList.remove('visible');
967 }, 3200);
968 }
969
970 function checkAllNPCsTalked() {
971 if (boardingShown) return;
972 boardingShown = true;
973
974 // Hide bubbles
975 Object.values(npcs).forEach(npc => {
976 npc.bubble.classList.remove('visible');
977 });
978
979 // Brief pause then transition
980 const boardingStage = document.getElementById('boardingStage');
981 const platformStage = document.getElementById('platformStage');
982
983 boardingStage.classList.add('active');
984 setTimeout(() => {
985 boardingStage.classList.add('visible');
986 }, 50);
987
988 setTimeout(() => {
989 platformStage.style.opacity = '0';
990 platformStage.style.transition = 'opacity 0.8s ease';
991 setTimeout(() => {
992 platformStage.classList.remove('active');
993 platformStage.style.opacity = '';
994 platformStage.style.transition = '';
995 }, 800);
996 }, 300);
997 }
998
999 // ============================
1000 // Boarding back button
1001 // ============================
1002 document.getElementById('boardingBackBtn').addEventListener('click', function() {
1003 const boardingStage = document.getElementById('boardingStage');
1004 const platformStage = document.getElementById('platformStage');
1005
1006 boardingStage.classList.remove('visible');
1007 setTimeout(() => {
1008 boardingStage.classList.remove('active');
1009 platformStage.classList.add('active');
1010 }, 600);
1011
1012 boardingShown = false;
1013 });
1014
1015 // ============================
1016 // Start button
1017 // ============================
1018 document.getElementById('startBtn').addEventListener('click', function() {
1019 const overlay = document.getElementById('startOverlay');
1020 const platform = document.getElementById('platformStage');
1021 overlay.classList.add('fade-out');
1022 setTimeout(() => {
1023 overlay.style.display = 'none';
1024 platform.classList.add('active');
1025 }, 800);
1026 });
1027
1028 // ============================
1029 // Departure board clock
1030 // ============================
1031 function updateDepartureTimes() {
1032 const now = new Date();
1033 const h = now.getHours().toString().padStart(2, '0');
1034 const m = now.getMinutes().toString().padStart(2, '0');
1035 const s = now.getSeconds();
1036
1037 // Nozomi departs at next even-hour :00, Hikari +30 min after
1038 let nozomiMin = (Math.floor(now.getMinutes() / 10) + 1) * 10 % 60;
1039 let nozomiH = now.getHours();
1040 if (nozomiMin <= now.getMinutes()) nozomiH = (nozomiH + 1) % 24;
1041
1042 let hikariMin = (nozomiMin + 18) % 60;
1043 let hikariH = nozomiH;
1044 if (hikariMin < nozomiMin) hikariH = (hikariH + 1) % 24;
1045
1046 document.getElementById('nozomiTime').textContent =
1047 nozomiH.toString().padStart(2,'0') + ':' + nozomiMin.toString().padStart(2,'0');
1048 document.getElementById('hikariTime').textContent =
1049 hikariH.toString().padStart(2,'0') + ':' + hikariMin.toString().padStart(2,'0');
1050 }
1051 updateDepartureTimes();
1052 setInterval(updateDepartureTimes, 30000);
1053
1054 // ============================
1055 // Generate stars
1056 // ============================
1057 (function generateStars() {
1058 const layer = document.getElementById('starsLayer');
1059 for (let i = 0; i < 80; i++) {
1060 const star = document.createElement('div');
1061 star.className = 'star';
1062 star.style.left = Math.random() * 100 + '%';
1063 star.style.top = Math.random() * 100 + '%';
1064 const size = Math.random() * 2 + 1;
1065 star.style.width = size + 'px';
1066 star.style.height = size + 'px';
1067 star.style.animationDuration = (Math.random() * 3 + 2) + 's';
1068 star.style.animationDelay = (Math.random() * 4) + 's';
1069 layer.appendChild(star);
1070 }
1071 })();
1072 </script>
1073 </body>
1074 </html>
1075