← back to Model Arena

data/artifacts/0f04410546ad/qwen3-14b.html

634 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Baguette Brawl — Duel de Paris</title>
  <style>
    :root {
      --bg: #f7efe6;
      --surface: #fff9f1;
      --text: #3a2c22;
      --muted: #a08c7a;
      --accent: #c65f38;
      --accent2: #e0a458;
      --p1: #c65f38;
      --p2: #4b6b7a;
      --shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
      --shadow-hover: 0 16px 48px rgba(0, 0, 0, 0.18);
      --glow: 0 0 24px rgba(198, 95, 56, 0.35);
      --font-display: 'Didot', 'Bodoni MT', 'Playfair Display', Georgia, serif;
      --font-body: -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
    }

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    html,
    body {
      height: 100%;
      background: radial-gradient(120% 120% at 50% 0%, #20222e 0%, #12131a 55%, #0a0a0f 100%);
      color: var(--text);
      font-family: var(--font-body);
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 100%;
      padding: 20px;
      overflow: hidden;
    }

    #wrap {
      position: relative;
      width: 960px;
      max-width: 100%;
      aspect-ratio: 960 / 540;
      border-radius: 14px;
      overflow: hidden;
      box-shadow: var(--shadow);
      background: #1a1720;
    }

    canvas {
      display: block;
      width: 100%;
      height: 100%;
      image-rendering: auto;
    }

    .overlay {
      position: absolute;
      inset: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
      background: linear-gradient(160deg, rgba(18, 15, 22, 0.86), rgba(30, 20, 14, 0.92));
      backdrop-filter: blur(2px);
      gap: 16px;
      padding: 32px;
      color: var(--bg);
      z-index: 10;
    }

    .overlay.hide {
      display: none;
    }

    .kicker {
      font-size: 12px;
      letter-spacing: 0.15em;
      text-transform: uppercase;
      color: var(--accent2);
      font-weight: 600;
      animation: fadeUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
    }

    h1 {
      font-family: var(--font-display);
      font-weight: 800;
      font-size: clamp(38px, 7vw, 67px);
      line-height: 0.98;
      color: var(--surface);
      letter-spacing: -0.01em;
      text-shadow: 0 2px 24px rgba(224, 164, 88, 0.25);
      animation: fadeUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
      animation-delay: 0.09s;
    }

    h1 .em {
      color: var(--accent2);
      font-style: italic;
    }

    .sub {
      max-width: 520px;
      color: #e8dfd2;
      font-size: 16px;
      line-height: 1.5;
      animation: fadeUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
      animation-delay: 0.18s;
    }

    .btns {
      display: flex;
      gap: 14px;
      flex-wrap: wrap;
      justify-content: center;
      animation: fadeUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
      animation-delay: 0.27s;
    }

    button.play {
      font-family: inherit;
      cursor: pointer;
      border: none;
      border-radius: 12px;
      padding: 15px 26px;
      font-size: 15px;
      font-weight: 700;
      letter-spacing: 0.02em;
      color: #fff;
      background: linear-gradient(135deg, var(--accent), #a8471f);
      box-shadow: var(--shadow);
      transition: transform 0.16s ease, box-shadow 0.16s ease;
    }

    button.play.alt {
      background: linear-gradient(135deg, #5c7d8f, #38505d);
      box-shadow: var(--shadow);
    }

    button.play:hover {
      transform: translateY(-2px);
      box-shadow: 0 16px 40px rgba(198, 95, 56, 0.5);
    }

    button.play.alt:hover {
      box-shadow: 0 16px 40px rgba(75, 107, 122, 0.5);
    }

    .controls {
      display: flex;
      gap: 32px;
      flex-wrap: wrap;
      justify-content: center;
      margin-top: 6px;
      animation: fadeUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
      animation-delay: 0.36s;
    }

    .ctrlcard {
      background: rgba(255, 249, 241, 0.06);
      border: 1px solid rgba(224, 164, 88, 0.22);
      border-radius: 12px;
      padding: 14px 18px;
      min-width: 190px;
      text-align: left;
    }

    .ctrlcard h4 {
      font-size: 12px;
      letter-spacing: 0.15em;
      text-transform: uppercase;
      color: var(--accent2);
      margin-bottom: 8px;
    }

    .ctrlcard .row {
      display: flex;
      justify-content: space-between;
      font-size: 13px;
      color: #e0d7c9;
      margin: 3px 0;
    }

    kbd {
      background: #2a2530;
      border: 1px solid #4a4150;
      border-bottom-width: 2px;
      border-radius: 5px;
      padding: 1px 7px;
      font-family: ui-monospace, Menlo, monospace;
      font-size: 12px;
      color: #f2e9dd;
    }

    .result-name {
      font-family: var(--font-display);
      font-style: italic;
    }

    .foot {
      position: absolute;
      bottom: 12px;
      font-size: 11px;
      letter-spacing: 0.15em;
      text-transform: uppercase;
      color: var(--muted);
    }

    @keyframes fadeUp {
      from {
        opacity: 0;
        transform: translateY(28px);
      }
      to {
        opacity: 1;
        transform: none;
      }
    }
  </style>
</head>
<body>
  <div id="wrap">
    <canvas id="c" width="960" height="540"></canvas>

    <!-- MENU -->
    <div id="menu" class="overlay">
      <div class="kicker">Sous la lune de Montmartre</div>
      <h1>Baguette <span class="em">Brawl</span></h1>
      <p class="sub">Two crusty duelists face off beneath the Eiffel Tower. Swing your loaf, guard your crust, and knock the crumbs out of your rival. First to <b>2 rounds</b> wins the boulangerie.</p>
      <div class="btns">
        <button class="play" data-mode="1">Solo vs. Chef IA</button>
        <button class="play alt" data-mode="2">Deux Joueurs</button>
      </div>
      <div class="controls">
        <div class="ctrlcard">
          <h4>Pierre · Joueur 1</h4>
          <div class="row"><span>Move / Jump</span><span><kbd>A</kbd><kbd>D</kbd><kbd>W</kbd></span></div>
          <div class="row"><span>Jab · Heavy</span><span><kbd>J</kbd><kbd>K</kbd></span></div>
          <div class="row"><span>Guard</span><span><kbd>L</kbd></span></div>
        </div>
        <div class="ctrlcard">
          <h4>Gaston · Joueur 2</h4>
          <div class="row"><span>Move / Jump</span><span><kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd></span></div>
          <div class="row"><span>Jab · Heavy</span><span><kbd>,</kbd><kbd>.</kbd></span></div>
          <div class="row"><span>Guard</span><span><kbd>/</kbd></span></div>
        </div>
      </div>
      <div class="foot">Un jeu de pain · Paris</div>
    </div>

    <!-- ROUND / RESULT -->
    <div id="result" class="overlay hide">
      <div class="kicker" id="resKicker">Manche terminée</div>
      <h1 id="resTitle">—</h1>
      <p class="sub" id="resSub"></p>
      <div class="btns" id="resBtns"></div>
    </div>
  </div>

  <script>
    (() => {
      const cv = document.getElementById('c');
      const ctx = cv.getContext('2d');
      const W = 960, H = 540;
      const GROUND = 470;

      const menuEl = document.getElementById('menu');
      const resEl = document.getElementById('result');
      const resTitle = document.getElementById('resTitle');
      const resSub = document.getElementById('resSub');
      const resKicker = document.getElementById('resKicker');
      const resBtns = document.getElementById('resBtns');

      let AC = null;
      function actx() {
        if (!AC) {
          try {
            AC = new (window.AudioContext || window.webkitAudioContext)();
          } catch (e) {}
        }
        return AC;
      }

      function tone(freq, dur, type = 'sine', vol = 0.15, slideTo = null) {
        const a = actx();
        if (!a) return;
        const o = a.createOscillator(), g = a.createGain();
        o.type = type; o.frequency.setValueAtTime(freq, a.currentTime);
        if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, a.currentTime + dur);
        g.gain.setValueAtTime(vol, a.currentTime);
        g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + dur);
        o.connect(g); g.connect(a.destination); o.start(); o.stop(a.currentTime + dur + 0.02);
      }

      function noise(dur, vol = 0.2, hp = 800) {
        const a = actx();
        if (!a) return;
        const n = Math.floor(a.sampleRate * dur), buf = a.createBuffer(1, n, a.sampleRate), d = buf.getChannelData(0);
        for (let i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / n);
        const s = a.createBufferSource(); s.buffer = buf;
        const f = a.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = hp;
        const g = a.createGain(); g.gain.value = vol;
        s.connect(f); f.connect(g); g.connect(a.destination); s.start();
      }

      const sfx = {
        swing() { noise(0.12, 0.12, 1200); },
        hit() { noise(0.16, 0.32, 500); tone(120, 0.1, 'square', 0.14, 70); },
        block() { tone(880, 0.09, 'triangle', 0.16, 660); noise(0.05, 0.08, 2500); },
        jump() { tone(300, 0.12, 'sine', 0.1, 520); },
        ko() { tone(400, 0.5, 'sawtooth', 0.2, 80); },
        win() {
          tone(523, 0.12, 'triangle', 0.16);
          setTimeout(() => tone(659, 0.12, 'triangle', 0.16), 120);
          setTimeout(() => tone(784, 0.2, 'triangle', 0.18), 240);
        }
      };

      const keys = new Set();
      addEventListener('keydown', e => {
        const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
        if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' ', '/'].includes(e.key)) e.preventDefault();
        keys.add(k);
      });
      addEventListener('keyup', e => {
        const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
        keys.delete(k);
      });

      function makeFighter(x, facing, cfg) {
        return {
          x, y: GROUND, vx: 0, vy: 0, facing, onGround: true,
          hp: 100, maxhp: 100,
          state: 'idle', atk: null, atkT: 0, atkDur: 0, atkHit: false,
          hurtT: 0, blockT: 0, blocking: false,
          swing: 0, walkPhase: 0,
          color: cfg.color, beret: cfg.beret, name: cfg.name,
          isAI: cfg.isAI || false, ctrl: cfg.ctrl,
          hitFlash: 0, wins: 0
        };
      }

      const P1CTRL = { left: 'a', right: 'd', jump: 'w', light: 'j', heavy: 'k', block: 'l' };
      const P2CTRL = { left: 'ArrowLeft', right: 'ArrowRight', jump: 'ArrowUp', light: ',', heavy: '.', block: '/' };

      let f1, f2, mode = 1, state = 'menu';
      let roundNum = 1, roundTime = 60, timeLeft = 60, roundActive = false, freeze = 0;
      let shake = 0, particles = [], clouds = [], stars = [];

      function initClouds() {
        clouds = [];
        for (let i = 0; i < 5; i++) {
          clouds.push({
            x: Math.random() * W, y: 40 + Math.random() * 120,
            s: 0.15 + Math.random() * 0.25, w: 80 + Math.random() * 90
          });
        }
        stars = [];
        for (let i = 0; i < 70; i++) {
          stars.push({
            x: Math.random() * W, y: Math.random() * 300,
            r: Math.random() * 1.4 + 0.3, t: Math.random() * 6
          });
        }
      }
      initClouds();

      function startGame(m) {
        mode = m; state = 'fight'; roundNum = 1;
        f1 = makeFighter(300, 1, { color: '#e0a458', beret: '#c62f2f', name: 'Pierre', ctrl: P1CTRL });
        f2 = makeFighter(660, -1, { color: '#d9974a', beret: '#2f4b6b', name: 'Gaston', ctrl: P2CTRL, isAI: (m === 1) });
        f1.wins = 0; f2.wins = 0;
        actx();
        startRound();
      }

      function startRound() {
        resEl.classList.add('hide'); menuEl.classList.add('hide');
        Object.assign(f1, { x: 300, y: GROUND, vx: 0, vy: 0, hp: 100, state: 'idle', atk: null, atkT: 0, hurtT: 0, swing: 0, facing: 1 });
        Object.assign(f2, { x: 660, y: GROUND, vx: 0, vy: 0, hp: 100, state: 'idle', atk: null, atkT: 0, hurtT: 0, swing: 0, facing: -1 });
        timeLeft = roundTime; roundActive = false; particles = []; freeze = 0;
        showAnnounce = `MANCHE ${roundNum}`; announceT = 1.6; countdownT = 3.2;
        state = 'fight';
      }

      let showAnnounce = '', announceT = 0, countdownT = 0;

      const ATK = {
        light: { dur: 0.34, active: [0.08, 0.18], reach: 78, dmg: 7, kb: 3.2, stun: 0.28 },
        heavy: { dur: 0.62, active: [0.22, 0.36], reach: 96, dmg: 16, kb: 8.5, stun: 0.5 }
      };

      function tryAttack(f, type) {
        if (f.state === 'attack' || f.state === 'hurt' || f.state === 'ko') return;
        if (!f.onGround && type === 'heavy') return;
        f.state = 'attack'; f.atk = type; f.atkT = 0; f.atkDur = ATK[type].dur; f.atkHit = false;
        sfx.swing();
      }

      function updateFighter(f, other, dt) {
        const c = f.ctrl;
        const canAct = f.state !== 'hurt' && f.state !== 'ko';
        f.blocking = false;

        if (f.onGround && f.state !== 'attack') {
          f.facing = other.x < f.x ? -1 : 1;
        }

        if (!f.isAI) {
          let mv = 0;
          if (canAct && f.state !== 'attack') {
            if (keys.has(c.left)) mv -= 1;
            if (keys.has(c.right)) mv += 1;
            if (keys.has(c.block)) { f.blocking = true; mv = 0; }
          }
          f.vx = mv * 3.4;
          if (canAct && keys.has(c.jump) && f.onGround) { f.vy = -13.4; f.onGround = false; sfx.jump(); }
          if (canAct) {
            if (keys.has(c.light)) tryAttack(f, 'light');
            else if (keys.has(c.heavy)) tryAttack(f, 'heavy');
          }
        } else {
          aiControl(f, other, dt);
        }

        if (f.blocking) f.blockT = Math.min(f.blockT + dt, 0.2);

        f.vy += 42 * dt;
        f.y += f.vy;
        f.x += f.vx;
        if (f.y >= GROUND) {
          f.y = GROUND;
          f.vy = 0;
          if (!f.onGround) { f.onGround = true; }
        } else {
          f.onGround = false;
        }
        f.x = Math.max(60, Math.min(W - 60, f.x));

        if (Math.abs(f.vx) > 0.2 && f.onGround) {
          f.walkPhase += dt * 12;
        } else {
          f.walkPhase *= 0.8;
        }

        if (f.state === 'attack') {
          f.atkT += dt;
          const a = ATK[f.atk];
          const p = f.atkT / f.atkDur;
          f.swing = swingCurve(p);
          if (!f.atkHit && p >= a.active[0] && p <= a.active[1]) {
            const dx = other.x - f.x;
            const inFront = (f.facing > 0 && dx > 0) || (f.facing < 0 && dx < 0);
            const dist = Math.abs(dx);
            const vgap = Math.abs(other.y - f.y);
            if (inFront && dist < a.reach + 40 && dist > 10 && vgap < 120) {
              f.atkHit = true;
              landHit(f, other, a);
            }
          }
          if (f.atkT >= f.atkDur) {
            f.state = 'idle'; f.atk = null; f.swing = 0;
          }
        } else {
          f.swing *= 0.7;
        }

        if (f.state === 'hurt') {
          f.hurtT -= dt;
          if (f.hurtT <= 0) f.state = 'idle';
        }
        if (f.hitFlash > 0) f.hitFlash -= dt;
        if (f.blockT > 0 && !f.blocking) f.blockT -= dt * 0.5;
      }

      function swingCurve(p) {
        if (p < 0.35) return -(p / 0.35) * 0.5;
        const q = (p - 0.35) / 0.65;
        return -0.5 + q * q * (3 - 2 * q) * 1.9;
      }

      function landHit(attacker, target, a) {
        if (target.state === 'ko') return;
        let dmg = a.dmg, kb = a.kb;
        const blocked = target.blocking && target.onGround && ((target.facing > 0 && attacker.x > target.x) || (target.facing < 0 && attacker.x < target.x));
        if (blocked) {
          dmg *= 0.18; kb *= 0.5;
          sfx.block();
          spawnParticles(target.x + target.facing * -20, target.y - 70, 6, '#ffe7b0', 3);
          shake = Math.max(shake, 4);
          target.blockT = 0.2;
        } else {
          sfx.hit();
          target.state = 'hurt'; target.hurtT = a.stun;
          spawnParticles(target.x, target.y - 70, 16, attacker.color, 6);
          spawnParticles(target.x, target.y - 80, 8, '#fff3dd', 5);
          target.hitFlash = 0.18;
          shake = Math.max(shake, a.dmg > 10 ? 11 : 6);
          freeze = a.dmg > 10 ? 0.07 : 0.035;
        }
        target.hp = Math.max(0, target.hp - dmg);
        target.vx = attacker.facing * kb;
        if (!blocked && a.dmg > 10) { target.vy = -4; target.onGround = false; }
        if (target.hp <= 0) {
          target.state = 'ko'; target.vx = attacker.facing * 5; target.vy = -8; target.onGround = false;
          sfx.ko();
        }
      }

      function aiControl(f, o, dt) {
        f._t = (f._t || 0) + dt;
        const dist = Math.abs(o.x - f.x);
        const dir = o.x < f.x ? -1 : 1;
        let mv = 0;

        if (f.state === 'attack' || f.state === 'hurt') {
          f.vx *= 0.85;
          return;
        }

        if (o.state === 'attack' && dist < 130 && Math.random() < 0.14) {
          f.blocking = true;
          f.vx = 0;
          return;
        }

        const ideal = 90;
        if (dist > ideal + 30) mv = dir;
        else if (dist < ideal - 30) mv = -dir;
        else {
          if (f._t > 0.35 + Math.random() * 0.4) {
            f._t = 0;
            if (dist < 100 && Math.random() < 0.55) tryAttack(f, 'heavy');
            else tryAttack(f, 'light');
          }
          mv = 0;
        }
        f.vx = mv * 3.4;
      }

      function draw(ctx, W, H) {
        ctx.clearRect(0, 0, W, H);

        // background
        ctx.fillStyle = '#0d0d0f';
        ctx.fillRect(0, 0, W, H);

        // clouds and stars
        for (const c of clouds) {
          ctx.save();
          ctx.translate(c.x, c.y);
          ctx.scale(c.s, c.s);
          ctx.fillStyle = 'white';
          ctx.beginPath();
          ctx.arc(0, 0, 50, 0, Math.PI * 2);
          ctx.fill();
          ctx.restore();
        }

        for (const s of stars) {
          ctx.beginPath();
          ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
          ctx.fillStyle = 'white';
          ctx.fill();
        }

        // draw fighters
        for (const f of [f1, f2]) {
          ctx.save();
          ctx.translate(f.x, f.y);
          ctx.scale(f.facing, 1);
          ctx.strokeStyle = 'white';
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.arc(0, 0, 24, 0, Math.PI * 2);
          ctx.stroke();
          ctx.beginPath();
          ctx.arc(0, -24, 10, 0, Math.PI * 2);
          ctx.fillStyle = f.color;
          ctx.fill();
          ctx.restore();
        }

        // draw text
        ctx.fillStyle = 'white';
        ctx.font = '20px serif';
        ctx.fillText(`Round ${roundNum}`, 400, 40);
        ctx.fillText(`Time: ${Math.floor(timeLeft)}`, 400, 60);
        ctx.fillText(`${f1.name}: ${f1.hp}`, 100, 40);
        ctx.fillText(`${f2.name}: ${f2.hp}`, 800, 40);

        if (showAnnounce) {
          ctx.fillStyle = 'white';
          ctx.font = '40px serif';
          ctx.fillText(showAnnounce, W / 2 - 120, H / 2);
        }
      }

      function update() {
        if (state === 'fight') {
          if (!roundActive) {
            timeLeft -= 0.03;
            if (timeLeft <= 0) {
              roundActive = true;
              startRound();
            }
          } else {
            updateFighter(f1, f2, 0.03);
            updateFighter(f2, f1, 0.03);
          }
        }
        draw(ctx, W, H);
        requestAnimationFrame(update);
      }

      const buttons = document.querySelectorAll('button.play');
      buttons.forEach(button => {
        button.addEventListener('click', () => {
          const mode = parseInt(button.getAttribute('data-mode'));
          startGame(mode);
        });
      });

      update();
    })();
  </script>
</body>
</html>