ブラウザゲーム.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
  <title>Endless Dodge ULTRA - Bullet & Boss</title>
  <style>
    :root{
      --bg1:#070816; --bg2:#0f1b38; --accent:#6ee7ff; --accent2:#9bffb7; --danger:#ff6b6b; --panel:rgba(255,255,255,.08);
      --text:#eaf2ff; --muted:#b5c0d0; --gold:#ffd166; --purple:#c4a7ff; --emerald:#86efac;
    }
    *{box-sizing:border-box}
    html,body{height:100%;}
    body{ margin:0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans JP";
      color:var(--text);
      background: radial-gradient(1200px 800px at 20% 10%, #1b2444 0%, var(--bg1) 50%), linear-gradient(160deg, var(--bg2), var(--bg1));
      overflow:hidden;}
    .wrap{position:fixed; inset:0; display:grid; grid-template-rows:auto 1fr auto;}
    header, footer{display:flex; gap:.75rem; align-items:center; justify-content:space-between; padding:.6rem .9rem; backdrop-filter: blur(6px); background:linear-gradient( to bottom, rgba(255,255,255,.06), rgba(255,255,255,.02)); border-bottom:1px solid rgba(255,255,255,.08)}
    header h1{font-size:1rem; margin:0; letter-spacing:.05em; font-weight:700}
    header .right{display:flex; gap:.5rem; align-items:center}
    .pill{ pointer-events:auto; border:1px solid rgba(255,255,255,.14); background:var(--panel); padding:.5rem .8rem; border-radius:999px; font-size:.9rem; color:var(--text); cursor:pointer; user-select:none; transition:transform .08s ease}
    .pill:active{ transform:scale(.97)}
    #gamePanel{ position:relative; display:grid; place-items:center;}
    canvas{ width: min(94vw, 800px); aspect-ratio: 9/16; border-radius: 18px; box-shadow: 0 10px 40px rgba(0,0,0,.5), inset 0 0 0 1px rgba(255,255,255,.06);
      background: radial-gradient(600px 500px at 50% 10%, rgba(110,231,255,.12), transparent 60%), linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.02));}
    .hud{ position:absolute; inset:0; pointer-events:none;}
    .row{ display:flex; justify-content:space-between; align-items:center; padding:10px;}
    .score{ font-variant-numeric: tabular-nums; font-size: clamp(18px, 3.5vw, 28px); text-shadow:0 1px 0 rgba(0,0,0,.5)}
    .muted{ color: var(--muted)}
    .center{ position:absolute; inset:0; display:grid; place-items:center;}
    .card{ pointer-events:auto; background:rgba(7,8,22,.92); border:1px solid rgba(255,255,255,.14); border-radius:16px; padding:20px; width:min(92vw, 480px); box-shadow:0 20px 60px rgba(0,0,0,.6)}
    .card h2{ margin:0 0 8px; font-size:1.25rem}
    .card p{ margin:.25rem 0; color:var(--muted)}
    .btn{ display:inline-flex; align-items:center; justify-content:center; gap:.5rem; padding:.7rem 1rem; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.06)); color:var(--text); cursor:pointer; font-weight:600}
    .btn:hover{ filter:brightness(1.08)}
    .btn.primary{ border-color: rgba(110,231,255,.5); box-shadow: 0 0 30px rgba(110,231,255,.15) inset}
    .grid{ display:grid; grid-template-columns:1fr 1fr; gap:.6rem}
    .touch{ position:absolute; inset:auto 0 10px 0; display:flex; justify-content:center; gap:12px; pointer-events:auto}
    .touch button{ width:clamp(64px, 22vw, 106px); aspect-ratio:1/1; border-radius:16px; border:1px solid rgba(255,255,255,.14); background:var(--panel); color:var(--text); font-weight:700; font-size:clamp(16px, 4.5vw, 22px); text-shadow:0 1px 0 rgba(0,0,0,.35)}
    .badge{border:1px solid rgba(255,255,255,.14); background:var(--panel); padding:.35rem .6rem; border-radius:999px; font-size:.75rem}
    .toast{ position:absolute; left:50%; top:14%; transform:translateX(-50%); pointer-events:none; opacity:0; transition: opacity .2s, transform .2s; background:rgba(0,0,0,.5); border:1px solid rgba(255,255,255,.18); padding:.35rem .7rem; border-radius:10px; font-weight:700}
    .toast.show{ opacity:1; transform:translate(-50%, -6px)}
    footer{ border-top:1px solid rgba(255,255,255,.08); border-bottom:none; justify-content:center}
    a{ color:var(--accent)}
    dialog{ border:none; border-radius:16px; background:rgba(7,8,22,.96); color:var(--text); width:min(92vw,560px); }
    dialog::backdrop{ background:rgba(0,0,0,.6); }
    .field{ display:flex; justify-content:space-between; align-items:center; gap:10px; padding:8px 0; }
    .range{ width:58% }
    .switch{ appearance:none; width:42px; height:24px; border-radius:999px; background:#445; position:relative; outline:none; cursor:pointer; }
    .switch:checked{ background:#2aa }
    .switch::after{ content:""; position:absolute; top:3px; left:3px; width:18px; height:18px; border-radius:50%; background:#fff; transition:left .15s}
    .switch:checked::after{ left:21px }
    .shop-item{ display:grid; grid-template-columns:1fr auto; gap:.4rem; align-items:center; padding:.5rem; border:1px solid rgba(255,255,255,.12); border-radius:12px; margin:.35rem 0; }
    .chip{ padding:.2rem .5rem; border:1px solid rgba(255,255,255,.16); border-radius:999px; font-size:.75rem; }
  </style>
</head>
<body>
  <div class="wrap">
    <header>
      <h1>Endless Dodge <span class="badge">ULTRA</span></h1>
      <div class="right">
        <span class="badge">💎 <span id="wallet">0</span></span>
        <button id="btnShop" class="pill" aria-label="shop">🛒 ショップ</button>
        <button id="btnSkins" class="pill" aria-label="skins">🎨 スキン</button>
        <button id="btnPause" class="pill" aria-label="pause">⏸</button>
        <button id="btnSound" class="pill" aria-label="sound">🔊</button>
        <button id="btnSettings" class="pill" aria-label="settings">⚙</button>
      </div>
    </header>
    <div id="gamePanel">
      <canvas id="game" width="360" height="640" aria-label="game canvas"></canvas>
      <div class="hud">
        <div class="row">
          <div class="score">
            <span id="score">0</span> pts
            · <span class="muted">Best:</span> <span id="best">0</span>
            · <span class="muted">Combo:</span> <span id="combo">x1.0</span>
            · <span class="muted">Stage:</span> <span id="stage">1</span>
          </div>
          <div class="row" style="gap:.5rem">
            <span class="badge" id="badges">⛨ 0 · 🧲 0 · ⏳ 0</span>
          </div>
        </div>
        <div class="center" id="overlayStart">
          <div class="card">
            <h2>避けて、撃って、強化して、ボスを倒せ!</h2>
            <p>← → / A・D で移動。<strong>Spaceでショット</strong>、<kbd>Shift</kbd>でダッシュ(無敵0.4s)。</p>
            <p>パワーアップ:⛨シールド / 🧲マグネット / ⏳スロウ。コンボでスコア倍率UP。</p>
            <p>ステージごとにボス戦。ボスは弾幕を発射。ショットでHPを削ろう。</p>
            <div class="grid" style="margin-top:10px">
              <button class="btn primary" id="btnStart">▶ ゲーム開始</button>
              <button class="btn" id="btnHow">❓ 操作</button>
            </div>
            <div style="margin-top:10px" class="muted" id="missions"></div>
          </div>
        </div>
        <div class="center" id="overlayBoss" style="display:none">
          <div class="card" style="text-align:center">
            <h2>⚠ B O S S ⚠</h2>
            <p>弾幕を避けつつ、Spaceで撃て!Shiftダッシュも活用。</p>
            <button class="btn primary" id="btnBossGo">戦闘開始</button>
          </div>
        </div>
        <div class="center" id="overlayGameOver" style="display:none">
          <div class="card">
            <h2>ゲームオーバー</h2>
            <p>スコア: <strong id="finalScore">0</strong> / ベスト: <strong id="finalBest">0</strong> / 💎<strong id="earned">0</strong></p>
            <p>達成:<span id="finalMissions" class="muted">-</span></p>
            <div class="grid" style="margin-top:10px">
              <button class="btn primary" id="btnRetry">↻ リトライ</button>
              <button class="btn" id="btnHome">⌂ タイトル</button>
            </div>
          </div>
        </div>
        <div class="touch" id="touchControls" aria-hidden="true">
          <button id="leftBtn" aria-label="left">⟵</button>
          <button id="dashBtn" aria-label="dash">⇧</button>
          <button id="rightBtn" aria-label="right">⟶</button>
        </div>
        <div class="toast" id="toast">Ready</div>
      </div>
    </div>
    <footer>
      <small class="muted">© 2025 Endless Dodge ULTRA · 図形のみ · ローカル保存(設定/進行/ウォレット/実績)</small>
    </footer>
  </div>

  <!-- Settings / Shop / Skins (unchanged structure) -->
  <dialog id="dlgSettings">
    <form method="dialog" style="padding:16px">
      <h3 style="margin:0 0 8px">設定</h3>
      <div class="field"><span>難易度(速度倍率)</span><input class="range" id="rangeSpeed" type="range" min="0.8" max="1.6" step="0.05"></div>
      <div class="field"><span>画面シェイク</span><input id="chkShake" class="switch" type="checkbox"></div>
      <div class="field"><span>色弱モード(高コントラスト)</span><input id="chkCB" class="switch" type="checkbox"></div>
      <div class="field"><span>省エネ描画(★数減少)</span><input id="chkEco" class="switch" type="checkbox"></div>
      <div class="field"><span>操作ヒントの表示</span><input id="chkHints" class="switch" type="checkbox"></div>
      <div style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:10px">
        <button class="btn" value="cancel">閉じる</button>
        <button class="btn primary" id="btnSaveSettings" value="default">保存</button>
      </div>
    </form>
  </dialog>

  <dialog id="dlgShop"><form method="dialog" style="padding:16px"><h3 style="margin:0 0 8px">ショップ</h3><p class="muted">💎はプレイ後にスコアから換算(100pts ≒ 1💎)。</p><div id="shopList"></div><div style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:10px"><button class="btn" value="cancel">閉じる</button></div></form></dialog>
  <dialog id="dlgSkins"><form method="dialog" style="padding:16px"><h3 style="margin:0 0 8px">スキン</h3><div id="skinList"></div><div style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:10px"><button class="btn" value="cancel">閉じる</button></div></form></dialog>

  <script>
  // ===== Utilities & Persistence =====
  const clamp=(v,min,max)=>Math.max(min,Math.min(max,v));
  const rand=(a,b)=>Math.random()*(b-a)+a; const choice=a=>a[(Math.random()*a.length)|0];
  const storage={ get(k,def){ try{return JSON.parse(localStorage.getItem(k)) ?? def}catch{ return def }}, set(k,v){ localStorage.setItem(k, JSON.stringify(v)); } };

  const SAVE={ best:'ultra-best', opts:'ultra-opts', stats:'ultra-stats', wallet:'ultra-wallet', upgrades:'ultra-upgrades', missions:'ultra-missions', skin:'ultra-skin' };
  const opts = Object.assign({ speedMul:1.0, shake:true, colorblind:false, eco:false, hints:true }, storage.get(SAVE.opts, {})); storage.set(SAVE.opts, opts);
  const wallet = { gems: storage.get(SAVE.wallet, 0) };
  function addGems(n){ wallet.gems = Math.max(0, Math.floor(wallet.gems + n)); storage.set(SAVE.wallet, wallet.gems); walletEl.textContent = wallet.gems; }

  const upgrades = Object.assign({ startShield:0, magnetDur:0, dashCD:0, scoreMul:0, extraLife:0 }, storage.get(SAVE.upgrades, {}));
  function uLevel(name){ return upgrades[name]||0 } function saveUpgrades(){ storage.set(SAVE.upgrades, upgrades); buildShop(); }

  const skins = [
    {id:'default', name:'Default', cost:0, color:'#eaf2ff'},
    {id:'neon', name:'Neon Blue', cost:50, color:'#7ee0ff'},
    {id:'sun', name:'Sun Gold', cost:80, color:'#ffd166'},
    {id:'void', name:'Void Purple', cost:120, color:'#c4a7ff'},
    {id:'leaf', name:'Leaf Green', cost:120, color:'#86efac'}
  ];
  let currentSkin = storage.get(SAVE.skin, 'default');

  function toast(msg, t=1200){ const el=document.getElementById('toast'); el.textContent=msg; el.classList.add('show'); clearTimeout(el._t); el._t=setTimeout(()=>el.classList.remove('show'), t); }

  // ===== Audio =====
  const AudioKit=(()=>{ let ctx, enabled=false; function ensure(){ if(!ctx){ const C=window.AudioContext||window.webkitAudioContext; if(C){ ctx=new C(); }} return ctx }
    function beep(freq=440, dur=0.08, type='sine', gain=0.02){ if(!enabled) return; const c=ensure(); if(!c) return; const o=c.createOscillator(); const g=c.createGain(); o.type=type; o.frequency.setValueAtTime(freq,c.currentTime); g.gain.setValueAtTime(gain,c.currentTime); o.connect(g).connect(c.destination); const t=c.currentTime; o.start(t); o.stop(t+dur); }
    function arpeggio(){ if(!enabled) return; const c=ensure(); if(!c) return; const base=220; const seq=[0,4,7,12,7,4]; seq.forEach((st,i)=>{ const o=c.createOscillator(); const g=c.createGain(); o.type='triangle'; o.frequency.setValueAtTime(base*Math.pow(2,st/12), c.currentTime + i*0.08); g.gain.setValueAtTime(0.02, c.currentTime + i*0.08); o.connect(g).connect(c.destination); o.start(c.currentTime + i*0.08); o.stop(c.currentTime + i*0.08 + 0.1); }); }
    return{ enable(){ enabled=true; ensure(); }, disable(){ enabled=false; }, toggle(){ enabled=!enabled; if(enabled) ensure(); return enabled; }, hit(){ beep(120,0.18,'square',0.05); }, coin(){ beep(880,0.07,'triangle',0.03); }, tick(){ beep(660,0.02,'sine',0.015); }, power(){ beep(520,0.1,'sawtooth',0.04); }, dash(){ beep(240,0.06,'square',0.05); }, fanfare(){ arpeggio(); }, shoot(){ beep(720,0.04,'square',0.03); } }
  })();

  // ===== Canvas & World =====
  const canvas=document.getElementById('game'); const ctx=canvas.getContext('2d');
  let dpr=1; function resize(){ dpr=Math.max(1, Math.min(2, window.devicePixelRatio||1)); const w=canvas.clientWidth; const h=canvas.clientHeight; canvas.width=Math.round(w*dpr); canvas.height=Math.round(h*dpr); ctx.setTransform(dpr,0,0,dpr,0,0); }
  new ResizeObserver(resize).observe(canvas); window.addEventListener('orientationchange', resize); resize();

  const state={ running:false, over:false, t:0, score:0, best: storage.get(SAVE.best, 0), baseSpeed:120, speed:120, worldW:360, worldH:640, combo:1, comboTime:0, slowed:0, stage:1, boss:false };
  const fx={ shakeTime:0, shakeAmp:0 };
  const starCount = opts.eco? 40 : 90; const stars=[...Array(starCount)].map(()=>({x:rand(0,360), y:rand(0,640), s:rand(0.5,2), sp:rand(10,40)}));

  const player={ x:180, y:560, r:12, vx:0, speed:270, color:'#eaf2ff', alive:true, flash:0, shield:0, magnet:0, dashCD:0, dashT:0, extra:0, fireCD:0 };
  const obstacles=[]; const coins=[]; const lasers=[]; const particles=[]; const powerups=[]; const bullets=[]; // boss bullets
  const pbullets=[]; // player bullets

  // ===== Spawning =====
  let lastSpawn=0, spawnInt=0.9; let lastLaser=0, laserInt=6.0; let stageTime=0, nextBossAt=28; // seconds
  function spawnBlockRow(yOff=-40){ const gap = clamp(140 - state.t*0.02, 70, 150); const blockW = rand(40, 90); const leftW = rand(10, state.worldW - gap - blockW - 10); const rightX = leftW + gap + blockW; const moving = Math.random()<clamp(0.08 + state.t*0.0006, 0.08, 0.4); const speed = moving? rand(30, 90)* (Math.random()<0.5?-1:1) : 0; obstacles.push({x:0, y:yOff, w:leftW, h:16, vx:0}); obstacles.push({x:rightX, y:yOff, w: state.worldW - rightX, h:16, vx:0}); if(moving){ obstacles.push({x:leftW+4, y:yOff-18, w: blockW-8, h:10, vx:speed}); }
    const cx = leftW + gap/2 + rand(-gap*0.35, gap*0.35); const cluster = (Math.random()<0.6) ? 4 : 1; for(let i=0;i<cluster;i++) coins.push({x:cx + (cluster>1?(i-1.5)*10:0), y:yOff-20 - i*8, r:6, vy:0}); if(Math.random()<0.22) powerups.push({x:cx+rand(-gap*0.3,gap*0.3), y:yOff-36, r:8, kind: choice(['shield','magnet','slow'])}); }
  function spawnLaser(){ const side = Math.random()<0.5? 'L':'R'; const x = side==='L'? -40 : state.worldW+40; const dir = side==='L'? 1 : -1; lasers.push({x, y: rand(120, state.worldH-160), w:120, h:10, vx: 170*dir, life: 4}); }

  // ===== Boss & Bullet Hell =====
  let boss=null; let patternT=0, patternId=0, spiralAng=0; // patterns
  function enterBoss(){ state.boss=true; show(bossOverlay); }
  function startBoss(){ hide(bossOverlay); boss = { x: state.worldW/2, y: 160, r: 22, hp: 6 + state.stage*2, vx: 80 }; bullets.length=0; patternT=0; patternId=0; spiralAng=0; }
  function bossShootFan(){ // 扇状(自機狙い)
    const dx = player.x - boss.x; const dy = (player.y - boss.y); const base = Math.atan2(dy, dx); const n=5; const spread=0.6; for(let i=0;i<n;i++){ const a = base + (i-(n-1)/2)*spread/n; bullets.push({x:boss.x, y:boss.y, r:4, vx:Math.cos(a)*160, vy:Math.sin(a)*160}); }
  }
  function bossShootRing(){ // 全方位リング
    const n=14; for(let i=0;i<n;i++){ const a = (i/n)*Math.PI*2; bullets.push({x:boss.x, y:boss.y, r:3.5, vx:Math.cos(a)*120, vy:Math.sin(a)*120}); }
  }
  function bossShootSpiral(){ // 渦巻き
    const a1 = spiralAng; const a2 = spiralAng + Math.PI; spiralAng += 0.35; bullets.push({x:boss.x, y:boss.y, r:3.5, vx:Math.cos(a1)*150, vy:Math.sin(a1)*150}); bullets.push({x:boss.x, y:boss.y, r:3.5, vx:Math.cos(a2)*150, vy:Math.sin(a2)*150}); }

  function updateBoss(dt){ if(!boss) return; boss.x += boss.vx*dt; if(boss.x<40){ boss.x=40; boss.vx=Math.abs(boss.vx);} if(boss.x>state.worldW-40){ boss.x=state.worldW-40; boss.vx=-Math.abs(boss.vx);} // pattern timeline
    patternT += dt; if(patternId===0){ if(patternT>0.6){ bossShootFan(); patternT=0; if(Math.random()<0.25) patternId=1; } }
    else if(patternId===1){ bossShootSpiral(); if(patternT>2.4){ patternT=0; patternId=2; } }
    else if(patternId===2){ if(patternT>1.0){ bossShootRing(); patternT=0; if(Math.random()<0.5) patternId=0; else patternId=1; } }

    // move bullets
    for(const b of bullets){ b.x += b.vx*dt; b.y += b.vy*dt; }
    for(let i=bullets.length-1;i>=0;i--){ const b=bullets[i]; if(b.x<-40||b.x>state.worldW+40||b.y<-40||b.y>state.worldH+60) bullets.splice(i,1); }

    // hit player
    for(const b of bullets){ const dx=player.x-b.x, dy=player.y-b.y; if(dx*dx+dy*dy <= (player.r+b.r)*(player.r+b.r)){ if(player.dashT<=0){ if(player.shield>0){ player.shield-=1; emit(player.x,player.y,12,'#6ee7ff'); } else if(player.extra>0){ player.extra--; toast('Extra Life!'); } else { return gameOver(); } } } }
  }
  function damageBoss(dmg=1){ if(!boss) return; boss.hp-=dmg; emit(boss.x,boss.y,16,'#c4a7ff'); if(boss.hp<=0){ boss=null; state.boss=false; state.stage++; stageTime=0; nextBossAt = clamp(26 - state.stage, 18, 26); addScore(200); toast(`Stage ${state.stage} クリア!`); AudioKit.fanfare(); }
  }

  function emit(x,y, n=8, col='#a8ffce'){ for(let i=0;i<n;i++){ particles.push({x,y, vx:rand(-90,90), vy:rand(-120,-40), life: rand(.3,.75), col}) } }

  // ===== Input =====
  let left=false, right=false, dashReq=false, shootHold=false;
  window.addEventListener('keydown',e=>{
    if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A') left=true;
    if(e.key==='ArrowRight'||e.key==='d'||e.key==='D') right=true;
    if(e.code==='Space'){ shootHold=true; e.preventDefault(); }
    if(e.key==='Shift') dashReq=true;
  });
  window.addEventListener('keyup',e=>{
    if(e.key==='ArrowLeft'||e.key==='a'||e.key==='A') left=false;
    if(e.key==='ArrowRight'||e.key==='d'||e.key==='D') right=false;
    if(e.code==='Space') shootHold=false;
  });

  const leftBtn=document.getElementById('leftBtn'); const rightBtn=document.getElementById('rightBtn'); const dashBtn=document.getElementById('dashBtn');
  const tp=document.getElementById('touchControls'); const isMobile = /Mobi|Android/i.test(navigator.userAgent); tp.style.display = isMobile? 'flex':'none';
  const press=(b)=>{ b.dataset.down='1'; if(b===leftBtn) left=true; else if(b===rightBtn) right=true; else dashReq=true; };
  const release=(b)=>{ b.dataset.down='0'; if(b===leftBtn) left=false; else if(b===rightBtn) right=false; };
  [leftBtn,rightBtn,dashBtn].forEach(b=>{ b.addEventListener('pointerdown',()=>press(b)); b.addEventListener('pointerup',()=>release(b)); b.addEventListener('pointerleave',()=>release(b)); });
  // mobile taps: single tap=shot, two-finger=dash
  canvas.addEventListener('touchstart',e=>{ if(e.touches.length>=2) { dashReq=true; } else { shootOnce(); } }, {passive:true});
  // desktop click to shoot too
  canvas.addEventListener('mousedown', shootOnce);

  // ===== Loop =====
  let last=performance.now(); function loop(t){ const dt=Math.min(0.033,(t-last)/1000); last=t; if(state.running) update(dt); draw(dt); requestAnimationFrame(loop); } requestAnimationFrame(loop);

  // ===== Mechanics =====
  const stats = { coins:0, dash:0, maxCombo:1, shield:0, score:0 };

  function reset(){ state.running=false; state.over=false; state.t=0; state.score=0; state.stage=1; stageTime=0; nextBossAt=28; state.speed=state.baseSpeed*opts.speedMul; state.combo=1; state.comboTime=0; state.slowed=0; state.boss=false; fx.shakeTime=0; fx.shakeAmp=0; boss=null;
    obstacles.length=0; coins.length=0; particles.length=0; lasers.length=0; powerups.length=0; bullets.length=0; pbullets.length=0;
    player.x=state.worldW/2; player.alive=true; player.flash=0; player.shield=0; player.magnet=0; player.dashCD=Math.max(0,2.6 - uLevel('dashCD')*0.4); player.dashT=0; player.extra = uLevel('extraLife'); player.fireCD=0;
    if(uLevel('startShield')>0) player.shield = 0.8 + 0.4*uLevel('startShield');
    spawnBlockRow(0); updateUI(); }

  function start(){ state.running=true; hide(startOverlay); hide(gameoverOverlay); hide(bossOverlay); AudioKit.tick(); }
  function gameOver(){ state.running=false; state.over=true; player.alive=false; AudioKit.hit(); state.best=Math.max(state.best, Math.floor(state.score)); storage.set(SAVE.best, state.best); const earned = Math.floor((state.score * (1 + 0.1*uLevel('scoreMul')))/100); addGems(earned); finalScore.textContent = Math.floor(state.score); finalBest.textContent = state.best; earnedEl.textContent = earned; finalMissions.textContent = summarizeMissions(); show(gameoverOverlay); updateUI(); }

  function updateUI(){ scoreEl.textContent = Math.floor(state.score); bestEl.textContent = state.best; comboEl.textContent = 'x'+state.combo.toFixed(1); badgesEl.textContent = `⛨ ${Math.ceil(player.shield)} · 🧲 ${Math.ceil(player.magnet)} · ⏳ ${Math.ceil(state.slowed)}`; stageEl.textContent = state.stage; walletEl.textContent = wallet.gems; }

  function addScore(v){ state.score += v * (1 + 0.1*uLevel('scoreMul')) * state.combo; stats.score = Math.floor(state.score); }
  function addCombo(dt){ state.combo = clamp(state.combo + dt*0.05, 1, 5); state.comboTime = 1.8; stats.maxCombo = Math.max(stats.maxCombo, state.combo); }

  function doDash(){ if(player.dashT>0 || player.dashCD>0) return; player.dashT=0.4; player.dashCD=Math.max(0.8, 3.0 - uLevel('dashCD')*0.4); stats.dash++; AudioKit.dash(); toast('Dash!'); fx.shakeTime=0.12; fx.shakeAmp=4; }

  function applyPower(kind){ if(kind==='shield'){ player.shield = Math.max(player.shield, 1.5 + 0.2*uLevel('startShield')); stats.shield++; toast('Shield ⛨'); }
    else if(kind==='magnet'){ player.magnet = Math.max(player.magnet, 4.5 + 0.5*uLevel('magnetDur')); toast('Magnet 🧲'); }
    else if(kind==='slow'){ state.slowed = Math.max(state.slowed, 2.5); toast('Slow ⏳'); }
    AudioKit.power(); }

  function collideCircleRect(cx,cy,cr, r){ const tx=clamp(cx, r.x, r.x+r.w); const ty=clamp(cy, r.y, r.y+r.h); const dx=cx-tx, dy=cy-ty; return dx*dx+dy*dy <= cr*cr; }

  function tryFire(){ if(player.fireCD>0) return; // fire 1~3 shots based on combo
    const n = (state.combo>=3.5? 3 : (state.combo>=2.0? 2:1));
    for(let i=0;i<n;i++){
      const off = (n===1)?0:(i-(n-1)/2)*6; pbullets.push({x:player.x+off, y:player.y-player.r-2, r:3, vy:-380});
    }
    player.fireCD = Math.max(0.08, 0.22 - (state.combo-1)*0.02);
    AudioKit.shoot();
  }
  function shootOnce(){ tryFire(); }

  function update(dt){
    state.t += dt; stageTime += dt; const speedMul = opts.speedMul * (state.slowed>0? 0.55:1); state.speed = clamp(120 + state.t*6, 120, 540) * speedMul; spawnInt = clamp(0.9 - state.t*0.02, 0.26, 0.9); laserInt = clamp(6.0 - state.t*0.01, 3.0, 6.0);

    if(!state.boss && stageTime>=nextBossAt){ enterBoss(); }

    lastSpawn += dt; if(lastSpawn>=spawnInt && !state.boss){ lastSpawn=0; spawnBlockRow(-20); }
    lastLaser += dt; if(lastLaser>=laserInt && !state.boss){ lastLaser=0; spawnLaser(); }

    // Player movement & actions
    const dir = (right?1:0) - (left?1:0);
    const skinCol = skins.find(s=>s.id===currentSkin)?.color || '#eaf2ff'; player.color = skinCol;
    player.vx = dir * player.speed * (player.dashT>0? 1.6:1);
    player.x = clamp(player.x + player.vx * dt, player.r+2, state.worldW - player.r-2);
    if(dashReq){ doDash(); dashReq=false; }
    if(player.dashT>0) player.dashT-=dt; if(player.dashCD>0) player.dashCD-=dt;
    if(player.fireCD>0) player.fireCD-=dt; if(shootHold) tryFire();

    // Stars
    for(const s of stars){ s.y += (state.speed*0.2 + s.sp) * dt; if(s.y>state.worldH) { s.y -= state.worldH; s.x = rand(0,state.worldW);} }

    // Entities movement
    for(const o of obstacles){ o.y += state.speed * dt; o.x += (o.vx||0) * dt; if(o.x<0){ o.x=0; o.vx=Math.abs(o.vx||0);} if(o.x+o.w>state.worldW){ o.x=state.worldW-o.w; o.vx = -Math.abs(o.vx||0);} }
    for(const c of coins){ c.y += (state.speed*0.95) * dt; const ax = (player.magnet>0? (player.x - c.x)*1.6 : 0); const ay = (player.magnet>0? (player.y - c.y)*1.6 : 0); c.x += ax*dt; c.y += ay*dt; }
    for(const p of particles){ p.x += p.vx*dt; p.y += p.vy*dt; p.vy += 420*dt; p.life -= dt; }
    for(const l of lasers){ l.x += l.vx*dt; l.life -= dt; }
    for(const pb of pbullets){ pb.y += pb.vy*dt; }

    if(state.boss){ updateBoss(dt); }

    // Clean
    while(obstacles.length && obstacles[0].y>state.worldH+40) obstacles.shift();
    while(coins.length && coins[0].y>state.worldH+40) coins.shift();
    for(let i=particles.length-1;i>=0;i--) if(particles[i].life<=0) particles.splice(i,1);
    for(let i=lasers.length-1;i>=0;i--) if(lasers[i].life<=0 || lasers[i].x<-160 || lasers[i].x>state.worldW+160) lasers.splice(i,1);
    for(let i=pbullets.length-1;i>=0;i--) if(pbullets[i].y<-30) pbullets.splice(i,1);

    for(let i=powerups.length-1;i>=0;i--) if(powerups[i].y>state.worldH+40) powerups.splice(i,1);
    for(const u of powerups){ u.y += state.speed*0.9*dt; }

    // Collisions with hazards
    let hit=false; if(!state.boss){ for(const o of obstacles){ if(collideCircleRect(player.x,player.y,player.r, o)) { hit=true; break; } } for(const l of lasers){ const r={x:l.x-4, y:l.y-2, w:l.w+8, h:l.h+4}; if(collideCircleRect(player.x,player.y,player.r, r)) { hit=true; break; } } }
    if(hit && player.dashT<=0){ if(player.shield>0){ player.shield-=0.9; emit(player.x, player.y, 14, '#6ee7ff'); fx.shakeTime=0.18; fx.shakeAmp=6; } else if(player.extra>0){ player.extra--; toast('Extra Life!'); emit(player.x,player.y,12,'#86efac'); } else { player.flash=0.18; emit(player.x, player.y, 18, '#ff7777'); return gameOver(); } }

    // coins
    for(let i=coins.length-1;i>=0;i--){ const c=coins[i]; const dx=player.x-c.x, dy=player.y-c.y; if(dx*dx+dy*dy < (player.r+c.r)*(player.r+c.r)){ coins.splice(i,1); addScore(10); addCombo(0.25); stats.coins++; AudioKit.coin(); emit(c.x,c.y,6,'#ffd166'); if(state.boss && boss){ damageBoss(0.3); } } }

    // powerups
    for(let i=powerups.length-1;i>=0;i--){ const u=powerups[i]; const dx=player.x-u.x, dy=player.y-u.y; if(dx*dx+dy*dy < (player.r+u.r)*(player.r+u.r)){ powerups.splice(i,1); applyPower(u.kind); addScore(5); } }

    // player bullets vs boss
    if(boss){ for(let i=pbullets.length-1;i>=0;i--){ const pb=pbullets[i]; const dx=boss.x-pb.x, dy=boss.y-pb.y; if(dx*dx+dy*dy <= (boss.r+pb.r)*(boss.r+pb.r)){ pbullets.splice(i,1); damageBoss(1); addScore(2); } } }

    // Effects timers
    if(player.shield>0) player.shield=Math.max(0, player.shield-dt);
    if(player.magnet>0) player.magnet=Math.max(0, player.magnet-dt);
    if(state.slowed>0) state.slowed=Math.max(0, state.slowed-dt);
    if(player.flash>0) player.flash=Math.max(0, player.flash-0.016);
    if(state.comboTime>0){ state.comboTime-=dt; if(state.comboTime<=0) state.combo = Math.max(1, state.combo-0.1); }

    // Score by time
    addScore(dt*3); updateUI();
  }

  // ===== Rendering =====
  function draw(){ const w=canvas.width/dpr, h=canvas.height/dpr; const sx = (fx.shakeTime>0 && opts.shake)? (rand(-fx.shakeAmp,fx.shakeAmp)) : 0; const sy = (fx.shakeTime>0 && opts.shake)? (rand(-fx.shakeAmp,fx.shakeAmp)) : 0; if(fx.shakeTime>0) fx.shakeTime -= 1/60; ctx.save(); ctx.clearRect(0,0,w,h); ctx.translate(sx, sy);
    const obCol = opts.colorblind? 'rgba(255,255,255,.9)': 'rgba(255,255,255,.14)';
    ctx.save(); ctx.globalAlpha=0.9; for(const s of stars){ ctx.fillStyle = `rgba(255,255,255,${0.2 + s.s*0.2})`; ctx.fillRect(s.x, s.y, s.s, s.s); } ctx.restore();
    ctx.save(); ctx.globalAlpha=0.06; ctx.lineWidth=1; const grid=20; ctx.beginPath(); for(let x=0;x<w;x+=grid){ ctx.moveTo(x,0); ctx.lineTo(x,h);} for(let y=0;y<h;y+=grid){ ctx.moveTo(0,y); ctx.lineTo(w,y);} ctx.strokeStyle='white'; ctx.stroke(); ctx.restore();

    // coins
    ctx.save(); for(const c of coins){ ctx.beginPath(); ctx.arc(c.x, c.y, c.r, 0, Math.PI*2); ctx.fillStyle = opts.colorblind? '#ffbf00' : 'var(--gold)'; ctx.fill(); ctx.lineWidth=1; ctx.strokeStyle='rgba(0,0,0,.25)'; ctx.stroke(); } ctx.restore();
    // powerups
    ctx.save(); for(const u of powerups){ ctx.beginPath(); ctx.arc(u.x, u.y, u.r, 0, Math.PI*2); ctx.fillStyle = u.kind==='shield'? '#6ee7ff' : (u.kind==='magnet'? '#9bffb7' : '#c4a7ff'); ctx.fill(); ctx.strokeStyle='rgba(0,0,0,.3)'; ctx.stroke(); ctx.font='10px system-ui'; ctx.fillStyle='#001'; const sym = u.kind==='shield'? '⛨' : (u.kind==='magnet'? '🧲' : '⏳'); ctx.fillText(sym, u.x-6, u.y+3); } ctx.restore();
    // obstacles & lasers (no boss phase)
    if(!state.boss){ ctx.save(); ctx.fillStyle=obCol; for(const o of obstacles){ ctx.fillRect(o.x, o.y, o.w, o.h); } ctx.restore(); ctx.save(); for(const l of lasers){ const grad=ctx.createLinearGradient(l.x, l.y, l.x+l.w, l.y+l.h); grad.addColorStop(0,'rgba(255,90,90,.85)'); grad.addColorStop(1,'rgba(255,160,160,.5)'); ctx.fillStyle=grad; ctx.fillRect(l.x, l.y, l.w, l.h); } ctx.restore(); }
    // boss
    if(state.boss && boss){ ctx.save(); const g=ctx.createRadialGradient(boss.x-6,boss.y-6,4, boss.x,boss.y,boss.r+6); g.addColorStop(0,'#fff'); g.addColorStop(1,'#c4a7ff'); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(boss.x,boss.y,boss.r,0,Math.PI*2); ctx.fill(); ctx.fillStyle='rgba(255,255,255,.8)'; ctx.fillRect(boss.x-24,boss.y-boss.r-16,48,6); ctx.fillStyle='#ff6bcb'; const hpw = clamp((boss.hp/(6+state.stage*2))*48,0,48); ctx.fillRect(boss.x-24,boss.y-boss.r-16,hpw,6); ctx.restore(); ctx.save(); ctx.fillStyle='#ff9d9d'; for(const b of bullets){ ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,Math.PI*2); ctx.fill(); } ctx.restore(); }
    // player bullets
    ctx.save(); ctx.fillStyle='#aee3ff'; for(const pb of pbullets){ ctx.beginPath(); ctx.arc(pb.x,pb.y,pb.r,0,Math.PI*2); ctx.fill(); } ctx.restore();
    // player
    ctx.save(); if(player.flash>0){ ctx.shadowColor=getCSS('--danger', '#ff6b6b'); ctx.shadowBlur=18; }
    ctx.beginPath(); ctx.arc(player.x, player.y, player.r, 0, Math.PI*2); const grad=ctx.createRadialGradient(player.x-4,player.y-6,4, player.x,player.y, player.r+6); grad.addColorStop(0, '#ffffff'); grad.addColorStop(1, player.color||'#7ee0ff'); ctx.fillStyle=grad; ctx.fill(); if(player.shield>0){ ctx.globalAlpha=0.25+0.15*Math.sin(performance.now()/120); ctx.beginPath(); ctx.arc(player.x, player.y, player.r+6, 0, Math.PI*2); ctx.strokeStyle='#8ae9ff'; ctx.lineWidth=3; ctx.stroke(); ctx.globalAlpha=1; } if(player.dashT>0){ ctx.globalAlpha=0.5; ctx.beginPath(); ctx.arc(player.x - 10, player.y, player.r*0.9, 0, Math.PI*2); ctx.fillStyle='#bde3ff'; ctx.fill(); ctx.globalAlpha=1; } ctx.restore();
    // particles
    ctx.save(); for(const p of particles){ ctx.globalAlpha = clamp(p.life,0,1); ctx.fillStyle=p.col||'#a8ffce'; ctx.fillRect(p.x, p.y, 2,2); } ctx.restore();
    ctx.restore();
  }

  function getCSS(name, fallback){ return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; }

  // ===== UI wires =====
  const startOverlay=document.getElementById('overlayStart'); const gameoverOverlay=document.getElementById('overlayGameOver'); const bossOverlay=document.getElementById('overlayBoss');
  const scoreEl=document.getElementById('score'); const bestEl=document.getElementById('best'); const comboEl=document.getElementById('combo'); const stageEl=document.getElementById('stage'); const badgesEl=document.getElementById('badges');
  const btnStart=document.getElementById('btnStart'); const btnRetry=document.getElementById('btnRetry'); const btnHome=document.getElementById('btnHome'); const btnPause=document.getElementById('btnPause'); const btnSound=document.getElementById('btnSound'); const btnSettings=document.getElementById('btnSettings'); const btnShop=document.getElementById('btnShop'); const btnSkins=document.getElementById('btnSkins'); const btnBossGo=document.getElementById('btnBossGo');
  const dlgSettings=document.getElementById('dlgSettings'); const rangeSpeed=document.getElementById('rangeSpeed'); const chkShake=document.getElementById('chkShake'); const chkCB=document.getElementById('chkCB'); const chkEco=document.getElementById('chkEco'); const chkHints=document.getElementById('chkHints'); const missionsEl=document.getElementById('missions'); const walletEl=document.getElementById('wallet');
  const finalScore=document.getElementById('finalScore'); const finalBest=document.getElementById('finalBest'); const earnedEl=document.getElementById('earned'); const finalMissions=document.getElementById('finalMissions');

  function show(el){ el.style.display='grid'; } function hide(el){ el.style.display='none'; }
  btnStart.addEventListener('click',()=>{ start(); AudioKit.enable(); }); btnRetry.addEventListener('click',()=>{ reset(); start(); }); btnHome.addEventListener('click',()=>{ reset(); show(startOverlay); });
  btnPause.addEventListener('click',()=>{ if(!state.running) resume(); else togglePause(); }); btnSound.addEventListener('click',()=>{ const on = AudioKit.toggle(); btnSound.textContent = on ? '🔊' : '🔇'; if(on) AudioKit.tick(); });
  btnSettings.addEventListener('click',()=>{ rangeSpeed.value=opts.speedMul; chkShake.checked=opts.shake; chkCB.checked=opts.colorblind; chkEco.checked=opts.eco; chkHints.checked=opts.hints; dlgSettings.showModal(); });
  document.getElementById('btnSaveSettings').addEventListener('click',(e)=>{ e.preventDefault(); opts.speedMul=parseFloat(rangeSpeed.value); opts.shake=chkShake.checked; opts.colorblind=chkCB.checked; opts.eco=chkEco.checked; opts.hints=chkHints.checked; storage.set(SAVE.opts, opts); dlgSettings.close(); toast('設定を保存しました'); });

  // Shop & Skins
  const dlgShop=document.getElementById('dlgShop'); const shopList=document.getElementById('shopList');
  function buildShop(){ shopList.innerHTML=''; const items=[
    {key:'startShield', name:'開始時シールド', desc:'+0.4〜のシールドを付与', base:40, max:3},
    {key:'magnetDur', name:'マグネット延長', desc:'+0.5s/レベル', base:30, max:5},
    {key:'dashCD', name:'ダッシュCD短縮', desc:'-0.4s/レベル', base:45, max:4},
    {key:'scoreMul', name:'スコア倍率', desc:'+10%/レベル', base:60, max:5},
    {key:'extraLife', name:'エクストラライフ', desc:'1回だけミスを無効化', base:120, max:1},
  ]; items.forEach(it=>{ const lv=uLevel(it.key); const cost = Math.floor(it.base * Math.pow(1.6, lv)); const can = lv<it.max && wallet.gems>=cost; const row=document.createElement('div'); row.className='shop-item'; row.innerHTML=`<div><strong>${it.name}</strong> <span class="chip">Lv.${lv}/${it.max}</span><div class="muted" style="font-size:.85rem">${it.desc}</div></div><button class="btn ${can?'primary':''}" ${can?'':'disabled'}>${lv>=it.max?'MAX':`購入 💎${cost}`}</button>`; row.querySelector('button').onclick=()=>{ if(lv>=it.max) return; if(wallet.gems<cost){ toast('💎不足'); return; } addGems(-cost); upgrades[it.key]=(upgrades[it.key]||0)+1; saveUpgrades(); toast(`${it.name} Lv.${upgrades[it.key]}`); }; shopList.appendChild(row); }); }
  const dlgSkins=document.getElementById('dlgSkins'); const skinList=document.getElementById('skinList');
  function buildSkins(){ skinList.innerHTML=''; skins.forEach(s=>{ const owned = (s.cost===0) || storage.get('skin-'+s.id, false); const can = wallet.gems>=s.cost && !owned; const row=document.createElement('div'); row.className='shop-item'; row.innerHTML=`<div><strong>${s.name}</strong> <span class="chip" style="background:${s.color}; color:#000">●</span> ${s.cost?`<span class='muted'>/ 💎${s.cost}</span>`:'<span class="muted">/ Free</span>'}</div><div><button class="btn ${owned?'':'primary'}" data-id="${s.id}">${owned?(currentSkin===s.id?'使用中':'使用'):('購入')}</button></div>`; row.querySelector('button').onclick=()=>{ if(!owned){ if(wallet.gems<s.cost){ toast('💎不足'); return; } addGems(-s.cost); storage.set('skin-'+s.id,true); } currentSkin=s.id; storage.set(SAVE.skin, currentSkin); buildSkins(); toast(`${s.name} を装備`); }; skinList.appendChild(row); }); }
  btnShop.addEventListener('click',()=>{ buildShop(); dlgShop.showModal(); }); btnSkins.addEventListener('click',()=>{ buildSkins(); dlgSkins.showModal(); }); btnBossGo.addEventListener('click',()=>{ startBoss(); });

  function togglePause(){ if(!state.running || state.over) return; state.running=false; btnPause.textContent='▶'; toast('Pause'); }
  function resume(){ if(state.over) return; state.running=true; btnPause.textContent='⏸'; toast('Resume'); }

  // Missions
  function generateMissions(){ const pool=[
    {id:'c80', text:'コインを80枚集める', test: s=>s.coins>=80},
    {id:'dash4', text:'1プレイでダッシュを4回', test: s=>s.dash>=4},
    {id:'combo35', text:'コンボ倍率3.5達成', test: s=>s.maxCombo>=3.5},
    {id:'shield', text:'シールド取得', test: s=>s.shield>0},
    {id:'score1200', text:'スコア1200到達', test: s=>s.score>=1200},
  ]; const chosen=[]; while(chosen.length<3){ const m=choice(pool); if(!chosen.find(c=>c.id===m.id)) chosen.push(m);} return chosen; }
  let missions = storage.get(SAVE.missions, null); if(!missions){ missions=generateMissions(); storage.set(SAVE.missions, missions);} missionsEl.innerHTML = '<strong>本日のミッション</strong><br>• '+missions.map(m=>m.text).join('<br>• ');
  function summarizeMissions(){ const done = missions.filter(m=>m.test(stats)).map(m=>m.text); return (done.length? done.join(' / ') : 'なし'); }

  // Init
  state.best = storage.get(SAVE.best, 0); walletEl.textContent=wallet.gems; updateUI(); reset(); show(startOverlay);

  </script>
</body>
</html>

地震情報サイト

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>地震情報 - 完全版</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: var(--bg-color, #ffffff);
            color: var(--text-color, #000000);
            transition: background-color 0.3s, color 0.3s;
        }
        .container {
            max-width: 1200px;
            margin: 20px auto;
            padding: 20px;
            background: var(--card-bg, #ffffff);
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        h1 {
            text-align: center;
        }
        #map {
            height: 400px;
            margin-top: 20px;
        }
        .toggle-dark-mode {
            position: fixed;
            top: 10px;
            right: 10px;
            padding: 10px;
            background: #007BFF;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        @media (max-width: 768px) {
            .container {
                padding: 10px;
            }
        }
    </style>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    <button class="toggle-dark-mode" onclick="toggleDarkMode()">ダークモード切り替え</button>
    <div class="container">
        <h1>地震情報</h1>
        <div id="quakeStats">
            <canvas id="intensityChart" width="400" height="200"></canvas>
        </div>
        <div id="map"></div>
        <div id="regionData"></div>
    </div>

    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
    <script>
        const rssUrl = 'https://www.jma.go.jp/bosai/quake/data/list.xml';
        let quakeData = [];
        const regions = {};

        const map = L.map('map').setView([35.6895, 139.6917], 5);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© OpenStreetMap contributors'
        }).addTo(map);

        async function fetchEarthquakeData() {
            try {
                const response = await fetch(`https://api.allorigins.win/get?url=${encodeURIComponent(rssUrl)}`);
                const data = await response.json();
                const parser = new DOMParser();
                const xmlDoc = parser.parseFromString(data.contents, "text/xml");

                const items = xmlDoc.getElementsByTagName('item');
                quakeData = [];

                for (let i = 0; i < items.length; i++) {
                    const title = items[i].getElementsByTagName('title')[0].textContent;
                    const description = items[i].getElementsByTagName('description')[0].textContent;
                    const pubDate = items[i].getElementsByTagName('pubDate')[0].textContent;

                    const match = description.match(/震源地:(.+?)、緯度:(.+?)、経度:(.+?)、震度:(.+?)、/);
                    if (match) {
                        const location = match[1];
                        const lat = parseFloat(match[2]);
                        const lng = parseFloat(match[3]);
                        const intensity = parseFloat(match[4]);

                        quakeData.push({ title, location, lat, lng, intensity, pubDate });
                        if (!regions[location]) {
                            regions[location] = [];
                        }
                        regions[location].push({ intensity, pubDate });
                    }
                }

                displayQuakeData();
                updateChart();
            } catch (error) {
                console.error('地震情報の取得に失敗しました:', error);
            }
        }

        function displayQuakeData() {
            map.eachLayer(layer => {
                if (layer.options && layer.options.pane === "markerPane") {
                    map.removeLayer(layer);
                }
            });

            quakeData.forEach(quake => {
                L.circle([quake.lat, quake.lng], {
                    color: 'red',
                    fillColor: '#f03',
                    fillOpacity: 0.5,
                    radius: quake.intensity * 10000
                }).addTo(map)
                .bindPopup(`<strong>${quake.title}</strong><br>震源地: ${quake.location}<br>震度: ${quake.intensity}`);
            });
        }

        function updateChart() {
            const ctx = document.getElementById('intensityChart').getContext('2d');
            const labels = quakeData.map(q => q.pubDate);
            const intensities = quakeData.map(q => q.intensity);

            new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: [{
                        label: '震度',
                        data: intensities,
                        borderColor: 'rgba(255, 99, 132, 1)',
                        borderWidth: 2,
                        fill: false
                    }]
                }
            });
        }

        function toggleDarkMode() {
            const isDarkMode = document.body.style.getPropertyValue('--bg-color') === '#ffffff';
            document.body.style.setProperty('--bg-color', isDarkMode ? '#333333' : '#ffffff');
            document.body.style.setProperty('--text-color', isDarkMode ? '#ffffff' : '#000000');
            document.body.style.setProperty('--card-bg', isDarkMode ? '#444444' : '#ffffff');
        }

        fetchEarthquakeData();
        setInterval(fetchEarthquakeData, 60000); // 1分ごとに更新
    </script>
</body>
</html>

ECサイトGamazon

http://tyosuke20xx.com/Gamazon.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gamazon</title>
    <style>
        body {
            font-family: Arial, sans-serif;
        }
        .navbar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 1rem;
            background-color: #131921;
            color: white;
        }
        .navbar h1 {
            font-size: 1.5rem;
        }
        .navbar nav ul {
            list-style: none;
            display: flex;
        }
        .navbar nav ul li {
            padding: 0 10px;
        }
        .navbar nav ul li a {
            text-decoration: none;
            color: white;
        }
        .product {
            border: 1px solid #ccc;
            margin: 10px;
            padding: 10px;
            width: 200px;
            float: left;
        }
        .product img {
            width: 100%;
            height: auto;
        }
        .featured-products .carousel {
            display: flex;
            overflow-x: auto;
            scroll-snap-type: x mandatory;
        }
        .featured-products .carousel div {
            scroll-snap-align: start;
            flex: 0 0 90%;
            margin-right: 10px;
        }
    </style>
</head>
<body>
    <header>
        <div class="navbar">
            <h1>My Gmazon Store</h1>
            <input type="text" placeholder="商品を検索" id="search-box">
            <button onclick="searchProduct()">検索</button>
            <nav>
                <ul>
                    <li><a href="#">ホーム</a></li>
                    <li><a href="#">商品カテゴリ</a></li>
                    <li><a href="#">セール</a></li>
                    <li><a href="#">お問い合わせ</a></li>
                </ul>
            </nav>
        </div>
    </header>

    <main>
        <section class="featured-products">
            <h2>特選商品</h2>
            <div class="carousel">
                <!-- カルーセル用のJavaScriptで動的に商品を挿入 -->
            </div>
        </section>

        <section class="product-list">
            <h2>商品リスト</h2>
            <div class="products">
                <!-- 商品リスト -->
            </div>
        </section>

        <section class="customer-reviews">
            <h2>ユーザーレビュー</h2>
            <div class="reviews">
                <!-- レビュー -->
            </div>
        </section>
    </main>

    <footer>
        <p>© 2024 My Gmazon Store. All rights reserved.</p>
    </footer>

    <script>
        function searchProduct() {
            const searchInput = document.getElementById('search-box').value;
            alert('検索した商品: ' + searchInput);
        }

        // 他にもカルーセルの動きやユーザーレビューを動的に表示する関数を追加
    </script>
</body>
</html>

動画共有サイト

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>動画共有サイト</title>
    <style>
        /* スタイルは省略 */
    </style>
</head>
<body>
    <header>
        <h1>動画共有サイト</h1>
    </header>
    <main>
        <section id="video-container">
            <!-- 動画を表示 -->
            <iframe width="560" height="315" src="https://www.youtube.com/embed/Qkls4DCX_9I" frameborder="0" allowfullscreen></iframe>
        </section>
        <section id="comments-container">
            <!-- コメントを表示 -->
        </section>
        <section id="comment-form-container">
            <!-- コメントを投稿するフォーム -->
            <form id="comment-form">
                <textarea id="comment-input" rows="3" placeholder="コメントを入力してください"></textarea>
                <button type="submit">コメントを投稿</button>
            </form>
        </section>
        <section id="categories">
            <!-- カテゴリ一覧 -->
            <h2>カテゴリ</h2>
            <ul>
                <li><a href="#">音楽</a></li>
                <li><a href="#">スポーツ</a></li>
                <li><a href="#">ゲーム</a></li>
                <li><a href="#">ニュース</a></li>
            </ul>
        </section>
    </main>
    <footer>
        <!-- お気に入りボタン -->
        <button id="favorite-button">お気に入り</button>
        <!-- 検索フォーム -->
        <input type="text" id="search-input" placeholder="動画を検索">
        <button id="search-button">検索</button>
    </footer>
    <script>
        // コメントを追加する関数
        function addComment(comment) {
            var commentsContainer = document.getElementById('comments-container');
            var commentElement = document.createElement('div');
            commentElement.textContent = comment;
            commentsContainer.appendChild(commentElement);
        }

        // フォームの送信イベントを処理する
        document.getElementById('comment-form').addEventListener('submit', function(event) {
            event.preventDefault(); // フォームのデフォルトの動作を停止

            var commentInput = document.getElementById('comment-input');
            var commentText = commentInput.value.trim(); // 入力されたコメントを取得

            if (commentText !== '') {
                addComment(commentText); // コメントを追加
                commentInput.value = ''; // 入力欄をクリア
            }
        });

        // お気に入りボタンのクリックイベントを処理する
        document.getElementById('favorite-button').addEventListener('click', function() {
            alert('動画をお気に入りに追加しました!');
        });

        // 検索ボタンのクリックイベントを処理する
        document.getElementById('search-button').addEventListener('click', function() {
            var searchInput = document.getElementById('search-input').value.trim();
            alert('「' + searchInput + '」で検索しました!');
        });
    </script>
</body>
</html>

「初心者でも簡単!Webサービスの作り方を徹底解説」

1. 「初心者でも簡単!Webサービスの作り方を徹底解説」

1. 「初心者でも簡単!Webサービスの作り方を徹底解説」

初心者でも簡単!Webサービスの作り方を徹底解説

はじめに、Webサービスの概要となぜ作る必要があるのかを紹介します。

次に、開発に必要な知識と準備を整えましょう。HTMLとCSSの基礎知識、Javascriptの基礎知識、データベースの基礎知識が必要です。

開発環境のセットアップでは、テキストエディターの選択とインストール方法、LAMP/WAMP/MAMP環境の構築方法を詳しく解説します。

ウェブサイトデザインでは、カラースキームの選択やレイアウト設計について学びます。

バックエンド実装では、サーバー側スクリプト言語やフレームワーク使用するかどうかも考えます。

フロントエンド実装ではHTMLファイル・CSSファイル・Javascriptファイルを作成する手順を解説します。

データベース設計と実装ではER図作成やテーブル設計、SQLクエリ実行方法も詳しく紹介します。

最後にはサーバーへのデプロイ方法もお伝えしましょう。ドメインとホスティングの取得方法やFTPを使ったファイルアップロード方法を解説します。

初心者でも簡単にWebサービスを作るための手順をまとめました。ぜひ参考にしてみてください!

はじめに

Webサービスの作り方を徹底解説します。まずは、Webサービスの概要について説明します。Webサービスとは、インターネット上で提供されるアプリケーションや機能のことです。例えば、SNSやオンラインショッピングなどが代表的なWebサービスです。

次に、なぜWebサービスを作るのかについて考えてみましょう。自分のアイデアやビジネスを実現するためには、自分でWebサービスを作成する必要があります。また、プログラミングやデザインの知識を身につけることで、将来的なキャリアパスも広がります。

この記事では、初心者でも簡単に始められるように具体的な手順や必要な知識を解説していきます。

まずはHTMLとCSSの基礎知識から学んでいきましょう。HTMLはウェブページの構造を定義するための言語であり、CSSはウェブページの見た目を装飾するための言語です。

次にJavascriptの基礎知識も身につけましょう。Javascriptはウェブページに動的な機能を追加するための言語です。

さらに、データベースの基礎知識も必要です。データベースはWebサービスで使用するデータを管理するための仕組みです。

開発環境のセットアップも重要なステップです。まずはテキストエディターを選択し、インストールします。また、LAMP/WAMP/MAMP環境の構築方法も解説します。

次にウェブサイトのデザインについて考えてみましょう。カラースキームやレイアウトの設計が重要です。

バックエンドの実装では、サーバー側スクリプト言語とフレームワークの選択がポイントとなります。

フロントエンドではHTMLファイル、CSSファイル、Javascriptファイルを作成していきます。

次にデータベースの設計と実装を行います。ER図やテーブル設計、SQLクエリなどが必要です。

最後にサーバーへのデプロイ方法を解説します。ドメインとホスティングを取得し、FTPを使ってファイルアップロードします。

以上がWebサービス作り方全体像です。初心者でも簡単に始められるように、各セクションで詳しく解説していきます。

必要な知識と準備

HTMLとCSSの基礎知識

Webサービスを作るためには、HTMLとCSSの基礎知識が必要です。HTMLはウェブページの構造を定義するためのマークアップ言語であり、CSSはウェブページのスタイルやデザインを指定するためのスタイルシート言語です。

Javascriptの基礎知識

Javascriptはウェブページに動的な機能やインタラクティビティを追加するために使用されるプログラミング言語です。Webサービスでは、ユーザーとの対話やデータ処理などにJavascriptが活用されます。

データベースの基礎知識

Webサービスでは、ユーザー情報やコンテンツデータなどを保存・管理するためにデータベースが必要です。データベースの基本的な概念やSQL(Structured Query Language)クエリの書き方など、データベース操作に関する知識が求められます。

以上がWebサービスを作る上で必要な知識と準備です。これらの基礎的な内容を学習し、実際に手を動かして経験を積むことで、より高度なWebサービスの開発に取り組むことができるようになります。

開発環境のセットアップ

テキストエディターの選択とインストール方法

開発を始める前に、まずは適切なテキストエディターを選択し、インストールする必要があります。テキストエディターはコードを書くためのツールであり、使いやすさや機能性などが重要なポイントです。代表的なテキストエディターとしてはVisual Studio CodeやSublime Textがあります。これらのテキストエディターは無料で利用することができますので、自分に合ったものを選んでインストールしましょう。

LAMP/WAMP/MAMP環境の構築方法(OSごとに分けて解説)

Webサービスを作るためには、ローカル環境に開発用サーバーを構築する必要があります。一般的な方法としてLAMP/WAMP/MAMP環境があります。これらはそれぞれLinux/Windows/Mac OS向けに提供されており、Apache(Webサーバーソフトウェア)、MySQL(データベース管理システム)、PHP(プログラミング言語)から成り立っています。

LAMP環境を構築する場合、まずはApacheをインストールし、設定を行います。次にMySQLをインストールし、データベースの設定を行います。最後にPHPをインストールし、Apacheと連携させます。

WAMP環境やMAMP環境も同様の手順で構築しますが、OSごとに異なるインストーラーが提供されているため注意が必要です。

開発環境のセットアップはWebサービス作成の第一歩です。正しくセットアップすることで効率的な開発が可能となりますので、丁寧に手順を確認しながら進めましょう。

データベースの設計と実装

ER図の作成

データベースの設計を行う際には、まずER図を作成する必要があります。ER図は、エンティティ(テーブル)間の関係性を視覚的に表現したものです。エンティティとは、データベース内で扱う情報の単位であり、例えば「顧客」や「商品」といったものです。ER図を作成することで、どのようなエンティティが存在し、それらがどのような関係性を持っているかが一目でわかります。

テーブルの設計

次に、ER図を元にして各エンティティ(テーブル)ごとに具体的なカラム(列)やデータ型を定義します。例えば、「顧客」エンティティでは、「氏名」「住所」「電話番号」といったカラムが必要になるでしょう。また、各カラムには適切なデータ型(文字列や数値など)を指定することも重要です。

SQLクエリの実行

最後に、実際にデータベースにテーブルを作成し、データの追加や検索などの操作を行います。これにはSQL(Structured Query Language)と呼ばれる言語を使用します。SQLクエリは、データベースへの命令文であり、例えば「顧客情報を追加する」「商品情報を検索する」といった操作が含まれます。

データベースの設計と実装は、Webサービス開発において非常に重要な要素です。適切な設計と効率的な実装が行われることで、データの管理や処理がスムーズに行えるようになります。また、セキュリティやパフォーマンス面でも影響を与えるため、注意深く取り組む必要があります。

サーバーへのデプロイ

ドメインとホスティングの取得方法

Webサービスを公開するためには、まずドメインとホスティングを取得する必要があります。ドメインは、ウェブサイトのアドレスを指定するためのものであり、ユーザーがアクセスする際に入力するURLです。一方、ホスティングはウェブサイトを保存・公開するためのサーバー空間です。

まず、お好みのドメイン名を選んで登録します。多くのレジストラ(登録業者)が存在し、それぞれ料金や機能が異なるため比較検討して選ぶことが重要です。登録後にDNS設定を行い、自分が取得したドメイン名とホスティング先IPアドレスを紐付けます。

次に、ウェブサイト用のホスティングプランを選択します。共有ホストや専用サーバーなどさまざまなタイプがあります。予算やトラフィック量などに応じて最適なプランを選択しましょう。

FTPを使ったファイルアップロード方法

ウェブサイトのファイルをホスティングサーバーにアップロードするためには、FTP(File Transfer Protocol)を使用します。FTPクライアントソフトウェアを使って、自分のコンピュータからホスティングサーバーへファイルを転送します。

まず、FTPクライアントソフトウェアをダウンロードしてインストールします。有名なものとしてはFileZillaやCyberduckなどがあります。

次に、FTPクライアントソフトウェアで接続設定を行います。ホスティングプロバイダーから提供された接続情報(ホスト名、ポート番号、ユーザー名、パスワード)を入力し、「接続」または「ログイン」ボタンをクリックします。

接続が成功したら、自分のコンピュータ上のファイルエクスプローラーと同様に操作できるようになります。必要なファイルやディレクトリを選択し、「転送」または「アップロード」ボタンを押すことでファイルがサーバー上に転送されます。

以上が基本的なサーバーへのデプロイ方法です。ドメインとホスティングの取得、FTPを使ったファイルアップロードを行うことで、自分のウェブサイトをインターネット上に公開することができます。

Djanogo test.py

test.py

from django.test import TestCase
from django.urls import resolve

from snippets.views import snippet_new, snippet_edit, snippet_detail

class CreateSnippetTest(TestCase):
def test_should_resolve_snippet_new(self):
found = resolve(“/snippets/new/”)
self.assertEqual(snippet_new, found.func)

class SnippetDetailTest(TestCase):
def test_should_resolve_snippet_detail(self):
found = resolve(“/snippets/1/”)
self.assertEqual(snippet_detail, found.func)

class EditSnippetTest(TestCase):
def test_should_resolve_snippet_edit(self):
found = resolve(“/snippets/1/edit/”)
self.assertEqual(snippet_edit, found.func)