Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
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