llamameta commited on
Commit
264bbda
·
verified ·
1 Parent(s): 53d253e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +47 -169
index.html CHANGED
@@ -2,7 +2,7 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
- <title>One Piece Short: Nami's Fury (Easy Record)</title>
6
  <style>
7
  :root {
8
  --sky-top: #4FACFE;
@@ -63,7 +63,7 @@
63
  padding: 20px;
64
  text-align: center;
65
  }
66
- #guide-overlay { z-index: 110; display: none; /* Hidden initially */ }
67
 
68
  .btn-group { display: flex; gap: 15px; margin-top: 25px; }
69
 
@@ -81,22 +81,23 @@
81
  }
82
  #play-btn { background: #2ecc71; box-shadow: 0 5px 0 #27ae60; }
83
  #play-btn:hover { transform: translateY(-2px); background: #2efc71; }
84
-
85
  #rec-btn { background: #e74c3c; box-shadow: 0 5px 0 #c0392b; }
86
  #rec-btn:hover { transform: translateY(-2px); background: #ff6b6b; }
87
 
88
  .guide-step {
89
  background: #333;
90
- padding: 15px;
91
- margin: 10px;
92
  border-radius: 8px;
93
  text-align: left;
94
- width: 80%;
95
- max-width: 500px;
96
  font-size: 16px;
97
  border-left: 5px solid #FFD700;
 
98
  }
99
  .highlight { color: #FFD700; font-weight: bold; }
 
100
 
101
  #subtitle-box {
102
  position: absolute;
@@ -124,24 +125,13 @@
124
 
125
  #rec-status {
126
  position: absolute;
127
- top: 15px;
128
- right: 15px;
129
  background: rgba(231, 76, 60, 0.9);
130
- color: white;
131
- padding: 8px 20px;
132
- border-radius: 30px;
133
- font-weight: bold;
134
- font-size: 16px;
135
- opacity: 0;
136
- z-index: 95;
137
- display: flex;
138
- align-items: center;
139
- gap: 10px;
140
- }
141
- .rec-dot {
142
- width: 12px; height: 12px; background: #fff; border-radius: 50%;
143
- animation: pulse 1s infinite;
144
  }
 
145
  @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
146
 
147
  /* --- Animations --- */
@@ -172,8 +162,6 @@
172
 
173
  <div id="main-container">
174
  <div id="rec-status"><div class="rec-dot"></div> RECORDING...</div>
175
-
176
- <!-- MAIN ANIMATION STAGE -->
177
  <svg id="stage" viewBox="0 0 960 540" preserveAspectRatio="xMidYMid slice">
178
  <defs>
179
  <pattern id="wood-grain" width="100" height="20" patternUnits="userSpaceOnUse">
@@ -276,41 +264,35 @@
276
 
277
  <div id="subtitle-box"> <div id="subtitle"></div> </div>
278
 
279
- <!-- START OVERLAY -->
280
  <div id="overlay">
281
  <h1 style="color:#FFD700; text-shadow: 4px 4px #000; font-size: 48px; margin-bottom: 5px;">ONE PIECE SHORT</h1>
282
  <h2 style="color:#fff; font-family: sans-serif; letter-spacing: 2px; margin-top: 0;">NAMI'S FURY</h2>
283
  <div class="btn-group">
284
  <button id="play-btn" class="main-btn">▶ JUST PLAY</button>
285
- <button id="rec-btn" class="main-btn">🔴 START RECORDING & PLAY</button>
286
  </div>
287
  <p style="color:#bbb; font-size: 14px; margin-top: 30px;">*Sound Required*</p>
288
  </div>
289
 
290
- <!-- RECORDING GUIDE OVERLAY -->
291
  <div id="guide-overlay">
292
- <h2 style="color:#FFD700;">HOW TO RECORD WITH SOUND</h2>
293
- <div class="guide-step">1. In the next popup, click the <span class="highlight">Chrome Tab</span> (or 'Browser Tab') option at the top.</div>
294
- <div class="guide-step">2. Select THIS tab name from the list.</div>
295
- <div class="guide-step">3. <span class="highlight" style="color:#ff6b6b">IMPORTANT:</span> Check the box labeled <b>"Also share tab audio"</b> (bottom left of popup).</div>
296
- <div class="guide-step">4. Click <b>Share</b> to start.</div>
297
- <button id="confirm-rec-btn" class="main-btn" style="background:#FFD700; color:#000; margin-top:20px;">I UNDERSTAND - LET'S GO</button>
298
- <p style="color:#ccc; font-size:12px; margin-top:15px;">(If you click 'Cancel' on the browser popup, recording will fail)</p>
299
  </div>
300
-
301
  </div>
302
 
303
  <script>
304
  const el = (id) => document.getElementById(id);
305
  const state = { audioCtx: null, voices: [], mediaRecorder: null, recordedChunks: [], isRecording: false };
306
 
307
- // === AUDIO ENGINE ===
308
  function initAudio() { if(!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
309
  function playSound(type) {
310
  if (!state.audioCtx) return;
311
  const ctx = state.audioCtx; const t = ctx.currentTime;
312
  const mGain = ctx.createGain(); mGain.connect(ctx.destination);
313
-
314
  if (type === 'ocean') {
315
  const buf = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
316
  const data = buf.getChannelData(0); for (let i = 0; i < ctx.sampleRate * 2; i++) data[i] = Math.random() * 2 - 1;
@@ -341,164 +323,60 @@
341
  }
342
  }
343
 
344
- // === TTS ===
345
- function initVoices() {
346
- return new Promise(resolve => {
347
- let id = setInterval(() => {
348
- const v = window.speechSynthesis.getVoices();
349
- if (v.length !== 0) { state.voices = v; clearInterval(id); resolve(); }
350
- }, 50);
351
- });
352
- }
353
- function getVoice(char) {
354
- const v = state.voices; const en = v.filter(voice => voice.lang.startsWith('en')); const pool = en.length > 0 ? en : v;
355
- if (char === 'nami') return pool.find(voice => voice.name.includes('Zira') || voice.name.includes('Female') || voice.name.includes('Google US English')) || pool[0];
356
- else return pool.find(voice => voice.name.includes('David') || voice.name.includes('Male')) || pool[1] || pool[0];
357
- }
358
- function speak(text, char) {
359
- return new Promise(resolve => {
360
- const u = new SpeechSynthesisUtterance(text); u.voice = getVoice(char); u.lang = 'en-US';
361
- if (char === 'luffy') { u.rate = 1.4; u.pitch = 1.5; } else { u.rate = 1.15; u.pitch = 1.1; }
362
- u.volume = 1.0;
363
- u.onstart = () => { el('subtitle').textContent = (char === 'nami' ? 'NAMI: ' : 'LUFFY: ') + text; el('subtitle').style.opacity = 1; }
364
- u.onend = () => { el('subtitle').style.opacity = 0; setTimeout(resolve, 400); }
365
- window.speechSynthesis.speak(u);
366
- });
367
- }
368
 
369
- // === ANIMATION HELPERS ===
370
- function setFace(char, type) {
371
- if (char === 'luffy') { el('luffy-face-normal').classList.toggle('hidden', type === 'stupid'); el('luffy-face-stupid').classList.toggle('hidden', type !== 'stupid'); }
372
- else { el('nami-face-normal').classList.toggle('hidden', type === 'angry'); el('nami-face-angry').classList.toggle('hidden', type !== 'angry'); el('nami-vein').classList.toggle('hidden', type !== 'angry'); }
373
- }
374
-
375
- // === RECORDER ===
376
  async function startRecording() {
377
  try {
378
- // Attempt to get display media (screen sharing)
379
- const stream = await navigator.mediaDevices.getDisplayMedia({
380
- video: { cursor: "never", displaySurface: "browser" },
381
- audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }
382
- });
383
 
384
- // Check if user actually shared audio. Firefox often doesn't support this check well, but Chrome/Opera do.
385
- const audioTracks = stream.getAudioTracks();
386
- if (audioTracks.length === 0) {
387
- alert("WARNING: No audio track detected! Did you forget to check 'Also share tab audio'? The video will be silent.");
388
  }
389
 
390
  state.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
391
  state.recordedChunks = [];
392
  state.mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) state.recordedChunks.push(e.data); };
393
  state.mediaRecorder.onstop = downloadVideo;
394
-
395
  state.mediaRecorder.start();
396
  state.isRecording = true;
397
  el('rec-status').style.opacity = 1;
398
-
399
- // If user manually clicks "Stop Sharing" on the browser banner, stop recording cleanly
400
  stream.getVideoTracks()[0].onended = () => { if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') stopRecording(); };
401
  return true;
402
  } catch (err) {
403
- console.error("Recording failed:", err);
404
- // Catch "Permission Denied" error if they clicked Cancel
405
- alert("Recording failed or was cancelled. You must click 'Share' in the browser popup to record.");
406
  return false;
407
  }
408
  }
 
 
409
 
410
- function stopRecording() {
411
- if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') {
412
- state.mediaRecorder.stop();
413
- if (state.mediaRecorder.stream) state.mediaRecorder.stream.getTracks().forEach(track => track.stop());
414
- }
415
- state.isRecording = false;
416
- el('rec-status').style.opacity = 0;
417
- }
418
-
419
- function downloadVideo() {
420
- if (state.recordedChunks.length === 0) return;
421
- const blob = new Blob(state.recordedChunks, { type: 'video/webm' });
422
- const url = URL.createObjectURL(blob);
423
- const a = document.createElement('a');
424
- a.style.display = 'none'; a.href = url; a.download = 'one_piece_nami_fury.webm';
425
- document.body.appendChild(a); a.click();
426
- setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100);
427
- }
428
-
429
- // === TIMELINE ===
430
  async function runEpisode() {
431
- el('guide-overlay').style.display = 'none';
432
- el('overlay').style.opacity = 0; setTimeout(() => el('overlay').style.display = 'none', 500);
433
-
434
- playSound('ocean');
435
- await new Promise(r => setTimeout(r, 1500));
436
- el('luffy').classList.add('fidget');
437
- await speak("Nami! I'm so booored! When's the next island?", 'luffy');
438
- await speak("I'm hungry! Sanji locked the fridge again!", 'luffy');
439
- el('luffy').classList.remove('fidget');
440
- await speak("Luffy, shut up for a second! I'm trying to read this map.", 'nami');
441
- await speak("If you bother me one more time, I'm tripling your debt!", 'nami');
442
- await speak("Ehhh? Maps are boring...", 'luffy');
443
- setFace('luffy', 'stupid'); playSound('stretch');
444
- el('luffy-arm-l').style.transition = "all 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55)";
445
- el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-200,80 -250,20');
446
- await new Promise(r => setTimeout(r, 1200));
447
- setFace('nami', 'angry');
448
- await speak("I SAID SHUT UP!", 'nami');
449
- el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-60,50 -50,90');
450
- el('nami').style.transition = "transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)";
451
- el('nami').style.transform = "translate(250px, 320px) rotate(-25deg) scale(1.1)";
452
- await new Promise(r => setTimeout(r, 400));
453
- playSound('punch'); playSound('spin');
454
- el('stage').classList.add('shake-screen');
455
- el('nami').style.transition = "transform 0.1s";
456
- el('nami').style.transform = "translate(450px, 320px) rotate(15deg) scale(1.2)";
457
- el('impact-star').classList.remove('hidden'); el('impact-star').style.transition = "all 0.15s ease-out";
458
- el('impact-star').style.transform = "translate(580px,250px) scale(4) rotate(180deg)";
459
- el('luffy').classList.remove('breathe'); el('luffy').classList.add('luffy-spin-crash');
460
- await new Promise(r => setTimeout(r, 400));
461
- el('stage').classList.remove('shake-screen'); el('impact-star').style.opacity = 0;
462
- await new Promise(r => setTimeout(r, 2500));
463
- el('nami').style.transition = "transform 0.8s ease-in-out";
464
- el('nami').style.transform = "translate(300px, 320px) rotate(0deg)";
465
- setFace('nami', 'normal');
466
- await speak("*Sigh*... what an idiot captain. Such a pain.", 'nami');
467
- await new Promise(r => setTimeout(r, 1000));
468
-
469
  if (state.isRecording) stopRecording();
470
-
471
- el('overlay').style.display = 'flex'; el('overlay').style.opacity = 1;
472
- el('overlay').innerHTML = "<h1 style='color:white'>FINISH</h1><button onclick='location.reload()' style='padding:15px 30px; font-size:20px; cursor:pointer; border-radius:10px; border:none; background:#FFD700;'>REPLAY</button>";
473
  }
474
 
475
- // === BUTTON HANDLERS ===
476
- el('play-btn').onclick = async () => {
477
- el('play-btn').textContent = "LOADING...";
478
- initAudio(); await initVoices();
479
- runEpisode();
480
- };
481
-
482
- el('rec-btn').onclick = () => {
483
- el('overlay').style.opacity = 0; // Hide main overlay briefly to show guide clearly
484
- setTimeout(() => { el('overlay').style.display = 'none'; el('guide-overlay').style.display = 'flex'; }, 300);
485
- };
486
-
487
  el('confirm-rec-btn').onclick = async () => {
488
- el('confirm-rec-btn').disabled = true;
489
- el('confirm-rec-btn').textContent = "WAITING FOR PERMISSION...";
490
  initAudio(); await initVoices();
491
  const success = await startRecording();
492
- if (success) {
493
- runEpisode();
494
- } else {
495
- // Reset if failed/cancelled
496
- el('guide-overlay').style.display = 'none';
497
- el('overlay').style.display = 'flex';
498
- el('overlay').style.opacity = 1;
499
- el('confirm-rec-btn').disabled = false;
500
- el('confirm-rec-btn').textContent = "I UNDERSTAND - LET'S GO";
501
- }
502
  };
503
  </script>
504
  </body>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <title>One Piece Short: Nami's Fury (Universal Record)</title>
6
  <style>
7
  :root {
8
  --sky-top: #4FACFE;
 
63
  padding: 20px;
64
  text-align: center;
65
  }
66
+ #guide-overlay { z-index: 110; display: none; }
67
 
68
  .btn-group { display: flex; gap: 15px; margin-top: 25px; }
69
 
 
81
  }
82
  #play-btn { background: #2ecc71; box-shadow: 0 5px 0 #27ae60; }
83
  #play-btn:hover { transform: translateY(-2px); background: #2efc71; }
 
84
  #rec-btn { background: #e74c3c; box-shadow: 0 5px 0 #c0392b; }
85
  #rec-btn:hover { transform: translateY(-2px); background: #ff6b6b; }
86
 
87
  .guide-step {
88
  background: #333;
89
+ padding: 12px 15px;
90
+ margin: 8px;
91
  border-radius: 8px;
92
  text-align: left;
93
+ width: 85%;
94
+ max-width: 550px;
95
  font-size: 16px;
96
  border-left: 5px solid #FFD700;
97
+ line-height: 1.4;
98
  }
99
  .highlight { color: #FFD700; font-weight: bold; }
100
+ .alt-option { color: #3498db; font-weight: bold; }
101
 
102
  #subtitle-box {
103
  position: absolute;
 
125
 
126
  #rec-status {
127
  position: absolute;
128
+ top: 15px; right: 15px;
 
129
  background: rgba(231, 76, 60, 0.9);
130
+ color: white; padding: 8px 20px; border-radius: 30px;
131
+ font-weight: bold; font-size: 16px; opacity: 0; z-index: 95;
132
+ display: flex; align-items: center; gap: 10px;
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
+ .rec-dot { width: 12px; height: 12px; background: #fff; border-radius: 50%; animation: pulse 1s infinite; }
135
  @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
136
 
137
  /* --- Animations --- */
 
162
 
163
  <div id="main-container">
164
  <div id="rec-status"><div class="rec-dot"></div> RECORDING...</div>
 
 
165
  <svg id="stage" viewBox="0 0 960 540" preserveAspectRatio="xMidYMid slice">
166
  <defs>
167
  <pattern id="wood-grain" width="100" height="20" patternUnits="userSpaceOnUse">
 
264
 
265
  <div id="subtitle-box"> <div id="subtitle"></div> </div>
266
 
 
267
  <div id="overlay">
268
  <h1 style="color:#FFD700; text-shadow: 4px 4px #000; font-size: 48px; margin-bottom: 5px;">ONE PIECE SHORT</h1>
269
  <h2 style="color:#fff; font-family: sans-serif; letter-spacing: 2px; margin-top: 0;">NAMI'S FURY</h2>
270
  <div class="btn-group">
271
  <button id="play-btn" class="main-btn">▶ JUST PLAY</button>
272
+ <button id="rec-btn" class="main-btn">🔴 RECORD VIDEO</button>
273
  </div>
274
  <p style="color:#bbb; font-size: 14px; margin-top: 30px;">*Sound Required*</p>
275
  </div>
276
 
 
277
  <div id="guide-overlay">
278
+ <h2 style="color:#FFD700;">HOW TO RECORD (READ CAREFULLY)</h2>
279
+ <div class="guide-step">1. OPTION A: If <span class="highlight">'Chrome Tab'</span> is available, select it and select THIS tab name.</div>
280
+ <div class="guide-step alt-option">1. OPTION B (FALLBACK): If 'Tab' is missing, select <span class="highlight">'Entire Screen'</span> (Seluruh Layar).</div>
281
+ <div class="guide-step">2. <span class="highlight" style="color:#ff6b6b">CRUCIAL:</span> You MUST check the box <b>"Share system audio"</b> (or 'Bagikan audio') at the bottom of the popup. Without this, video will be silent.</div>
282
+ <div class="guide-step">3. Click <b>Share</b> to begin.</div>
283
+ <button id="confirm-rec-btn" class="main-btn" style="background:#FFD700; color:#000; margin-top:15px;">I UNDERSTAND - START</button>
 
284
  </div>
 
285
  </div>
286
 
287
  <script>
288
  const el = (id) => document.getElementById(id);
289
  const state = { audioCtx: null, voices: [], mediaRecorder: null, recordedChunks: [], isRecording: false };
290
 
 
291
  function initAudio() { if(!state.audioCtx) state.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
292
  function playSound(type) {
293
  if (!state.audioCtx) return;
294
  const ctx = state.audioCtx; const t = ctx.currentTime;
295
  const mGain = ctx.createGain(); mGain.connect(ctx.destination);
 
296
  if (type === 'ocean') {
297
  const buf = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
298
  const data = buf.getChannelData(0); for (let i = 0; i < ctx.sampleRate * 2; i++) data[i] = Math.random() * 2 - 1;
 
323
  }
324
  }
325
 
326
+ function initVoices() { return new Promise(r => { let id = setInterval(() => { const v = window.speechSynthesis.getVoices(); if (v.length !== 0) { state.voices = v; clearInterval(id); r(); } }, 50); }); }
327
+ function getVoice(char) { const v = state.voices; const en = v.filter(vc => vc.lang.startsWith('en')); const pool = en.length > 0 ? en : v; if (char === 'nami') return pool.find(vc => vc.name.includes('Zira') || vc.name.includes('Female')) || pool[0]; else return pool.find(vc => vc.name.includes('Male')) || pool[1] || pool[0]; }
328
+ function speak(text, char) { return new Promise(r => { const u = new SpeechSynthesisUtterance(text); u.voice = getVoice(char); u.lang = 'en-US'; if (char === 'luffy') { u.rate = 1.4; u.pitch = 1.5; } else { u.rate = 1.15; u.pitch = 1.1; } u.volume = 1.0; u.onstart = () => { el('subtitle').textContent = (char === 'nami' ? 'NAMI: ' : 'LUFFY: ') + text; el('subtitle').style.opacity = 1; }; u.onend = () => { el('subtitle').style.opacity = 0; setTimeout(r, 400); }; window.speechSynthesis.speak(u); }); }
329
+ function setFace(char, type) { if (char === 'luffy') { el('luffy-face-normal').classList.toggle('hidden', type === 'stupid'); el('luffy-face-stupid').classList.toggle('hidden', type !== 'stupid'); } else { el('nami-face-normal').classList.toggle('hidden', type === 'angry'); el('nami-face-angry').classList.toggle('hidden', type !== 'angry'); el('nami-vein').classList.toggle('hidden', type !== 'angry'); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
 
 
 
 
 
 
 
331
  async function startRecording() {
332
  try {
333
+ // Removed strict constraints to allow browser fallbacks more easily
334
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: { cursor: "never" }, audio: true });
 
 
 
335
 
336
+ // Basic check if audio track exists (browser dependent if it works)
337
+ if (stream.getAudioTracks().length === 0) {
338
+ // Optional: alert user here if you want strictly enforce audio, but let's let it slide as fallback.
339
+ console.warn("Audio track might be missing if 'Share Audio' wasn't checked.");
340
  }
341
 
342
  state.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
343
  state.recordedChunks = [];
344
  state.mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) state.recordedChunks.push(e.data); };
345
  state.mediaRecorder.onstop = downloadVideo;
 
346
  state.mediaRecorder.start();
347
  state.isRecording = true;
348
  el('rec-status').style.opacity = 1;
 
 
349
  stream.getVideoTracks()[0].onended = () => { if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') stopRecording(); };
350
  return true;
351
  } catch (err) {
 
 
 
352
  return false;
353
  }
354
  }
355
+ function stopRecording() { if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') { state.mediaRecorder.stop(); if (state.mediaRecorder.stream) state.mediaRecorder.stream.getTracks().forEach(track => track.stop()); } state.isRecording = false; el('rec-status').style.opacity = 0; }
356
+ function downloadVideo() { if (state.recordedChunks.length === 0) return; const blob = new Blob(state.recordedChunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = 'one_piece_nami_fury.webm'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); }, 100); }
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  async function runEpisode() {
359
+ el('guide-overlay').style.display = 'none'; el('overlay').style.opacity = 0; setTimeout(() => el('overlay').style.display = 'none', 500);
360
+ playSound('ocean'); await new Promise(r => setTimeout(r, 1500));
361
+ el('luffy').classList.add('fidget'); await speak("Nami! I'm so booored! When's the next island?", 'luffy'); await speak("I'm hungry! Sanji locked the fridge again!", 'luffy'); el('luffy').classList.remove('fidget');
362
+ await speak("Luffy, shut up for a second! I'm trying to read this map.", 'nami'); await speak("If you bother me one more time, I'm tripling your debt!", 'nami');
363
+ await speak("Ehhh? Maps are boring...", 'luffy'); setFace('luffy', 'stupid'); playSound('stretch'); el('luffy-arm-l').style.transition = "all 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55)"; el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-200,80 -250,20'); await new Promise(r => setTimeout(r, 1200));
364
+ setFace('nami', 'angry'); await speak("I SAID SHUT UP!", 'nami'); el('luffy-arm-l').setAttribute('d', 'M-35,10 Q-60,50 -50,90'); el('nami').style.transition = "transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)"; el('nami').style.transform = "translate(250px, 320px) rotate(-25deg) scale(1.1)"; await new Promise(r => setTimeout(r, 400));
365
+ playSound('punch'); playSound('spin'); el('stage').classList.add('shake-screen'); el('nami').style.transition = "transform 0.1s"; el('nami').style.transform = "translate(450px, 320px) rotate(15deg) scale(1.2)"; el('impact-star').classList.remove('hidden'); el('impact-star').style.transition = "all 0.15s ease-out"; el('impact-star').style.transform = "translate(580px,250px) scale(4) rotate(180deg)"; el('luffy').classList.remove('breathe'); el('luffy').classList.add('luffy-spin-crash'); await new Promise(r => setTimeout(r, 400));
366
+ el('stage').classList.remove('shake-screen'); el('impact-star').style.opacity = 0; await new Promise(r => setTimeout(r, 2500));
367
+ el('nami').style.transition = "transform 0.8s ease-in-out"; el('nami').style.transform = "translate(300px, 320px) rotate(0deg)"; setFace('nami', 'normal'); await speak("*Sigh*... what an idiot captain. Such a pain.", 'nami'); await new Promise(r => setTimeout(r, 1000));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  if (state.isRecording) stopRecording();
369
+ el('overlay').style.display = 'flex'; el('overlay').style.opacity = 1; el('overlay').innerHTML = "<h1 style='color:white'>FINISH</h1><button onclick='location.reload()' style='padding:15px 30px; font-size:20px; cursor:pointer; border-radius:10px; border:none; background:#FFD700;'>REPLAY</button>";
 
 
370
  }
371
 
372
+ el('play-btn').onclick = async () => { el('play-btn').textContent = "LOADING..."; initAudio(); await initVoices(); runEpisode(); };
373
+ el('rec-btn').onclick = () => { el('overlay').style.opacity = 0; setTimeout(() => { el('overlay').style.display = 'none'; el('guide-overlay').style.display = 'flex'; }, 300); };
 
 
 
 
 
 
 
 
 
 
374
  el('confirm-rec-btn').onclick = async () => {
375
+ el('confirm-rec-btn').disabled = true; el('confirm-rec-btn').textContent = "WAITING FOR PERMISSION...";
 
376
  initAudio(); await initVoices();
377
  const success = await startRecording();
378
+ if (success) runEpisode();
379
+ else { el('guide-overlay').style.display = 'none'; el('overlay').style.display = 'flex'; el('overlay').style.opacity = 1; el('confirm-rec-btn').disabled = false; el('confirm-rec-btn').textContent = "I UNDERSTAND - START"; }
 
 
 
 
 
 
 
 
380
  };
381
  </script>
382
  </body>