← back to Model Arena

data/artifacts/0f04410546ad/qwen25-7b.html

409 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;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    background:radial-gradient(120% 120% at 50% 0%,#20222e 0%,#12131a 55%,#0a0a0f 100%);
    color:var(--text);
    font-family:-apple-system,'Segoe UI','Helvetica Neue',Arial,sans-serif;
    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:0 16px 48px rgba(0,0,0,.55), 0 0 0 1px rgba(224,164,88,.25);
    background:#1a1720;
  }
  canvas{display:block;width:100%;height:100%;image-rendering:auto}

  /* overlays */
  .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,.86),rgba(30,20,14,.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:.28em;text-transform:uppercase;
    color:var(--accent2);font-weight:600;
    animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;
  }
  h1{
    font-family:'Playfair Display',Georgia,'Bodoni MT',serif;
    font-weight:800;font-size:clamp(38px,7vw,67px);line-height:.98;
    color:var(--surface);letter-spacing:-.01em;
    text-shadow:0 2px 24px rgba(224,164,88,.25);
    animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.09s;
  }
  h1 .em{color:var(--accent2);font-style:italic}
  .sub{
    max-width:520px;color:#e8dfd2;font-size:16px;line-height:1.5;
    animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.18s;
  }
  .btns{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;
    animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.27s;}
  button.play{
    font-family:inherit;cursor:pointer;border:none;border-radius:12px;
    padding:15px 26px;font-size:15px;font-weight:700;letter-spacing:.02em;
    color:#fff;background:linear-gradient(135deg,var(--accent),#a8471f);
    box-shadow:0 8px 24px rgba(198,95,56,.4);transition:transform .16s ease,box-shadow .16s ease;
  }
  button.play.alt{background:linear-gradient(135deg,#5c7d8f,#38505d);box-shadow:0 8px 24px rgba(75,107,122,.4)}
  button.play:hover{transform:translateY(-2px);box-shadow:0 16px 40px rgba(198,95,56,.5)}
  button.play.alt:hover{box-shadow:0 16px 40px rgba(75,107,122,.5)}
  .controls{
    display:flex;gap:32px;flex-wrap:wrap;justify-content:center;margin-top:6px;
    animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.36s;
  }
  .ctrlcard{
    background:rgba(255,249,241,.06);border:1px solid rgba(224,164,88,.22);
    border-radius:12px;padding:14px 18px;min-width:190px;text-align:left;
  }
  .ctrlcard h4{font-size:12px;letter-spacing:.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:'Playfair Display',Georgia,serif;font-style:italic}
  .foot{position:absolute;bottom:12px;font-size:11px;letter-spacing:.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"><button class="play" data-mode="1">Rejouer</button></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');

// ---------- input ----------
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);
});

// ---------- fighter ----------
function makeFighter(x, facing, cfg) {
  return {
    x, y: GROUND, vx: 0, vy: 0, facing,
    hp: 100, maxhp: 100,
    state: 'idle', // idle | attack | hurt | ko
    atk: null, atkT: 0, atkDur: 0, atkHit: false,
    hurtT: 0, blockT: 0, blocking: false,
    swing: 0, // swing anim 0..1
    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: .15 + Math.random() *.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 + .3, t: Math.random() * 6 });
  }
}
initClouds();

function startGame(m) {
  mode = m; state = 'fight'; roundNum = 1;
  f1 = makeFighter(300, 1, { color: '#c65f38', beret: '#c62f2f', name: 'Pierre', ctrl: P1CTRL });
  f2 = makeFighter(660, -1, { color: '#e0a458', beret: '#2f4b6b', name: 'Gaston', ctrl: P2CTRL, isAI: (m === 1) });
  f1.wins = 0; f2.wins = 0;
  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;

// ---------- combat tuning ----------
const ATK = {
  light: { dur: .34, active: [.08, .18], reach: 78, dmg: 7, kb: 3.2, stun: .28 },
  heavy: { dur: .62, active: [.22, .36], reach: 96, dmg: 16, kb: 8.5, stun: .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;
  ctx.fillStyle = type === 'light' ? '#c65f38' : '#e0a458';
  ctx.fillRect(f.x - 10, GROUND + 20, 20, 20);
}

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

  // face opponent when idle-ish on ground
  if (f.onGround && f.state !== 'attack') {
    f.facing = other.x < f.x ? -1 : 1;
  }

  if (!f.isAI) {
    // horizontal movement
    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; ctx.fillStyle = '#c65f38'; ctx.fillRect(f.x, GROUND + 20, 20, 20); }
    if (canAct) {
      if (keys.has(c.light)) tryAttack(f, 'light');
      else if (keys.has(c.heavy)) tryAttack(f, 'heavy');
    }
  } else {
    aiControl(f, other, dt);
  }

  // block flash timer
  if (f.blocking) f.blockT = Math.min(f.blockT + dt, .2);

  // gravity and collision with ground
  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) > .2 && f.onGround) f.walkPhase += dt * 12; else f.walkPhase *= .8;

  // attack timeline
  if (f.state === 'attack') {
    f.atkT += dt;
    const a = ATK[f.atk];
    const p = f.atkT / f.atkDur;
    f.swing = swingCurve(p);
    // active window hit
    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 *= .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 * .5;

}

function swingCurve(p) {
  // windup then fast swing
  if (p < .35) return -(p / .35) * .5;         // wind back
  const q = (p - .35) / .65;
  return -0.5 + q * q * (3 - 2 * q) * 1.9;         // smooth forward
}

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 *= .5;
    ctx.fillStyle = '#ffe7b0';
    ctx.fillRect(target.x + target.facing * -20, target.y - 70, 6, 3);
    shake = Math.max(shake, 4);
    target.blockT = .2;
  } else {
    ctx.fillStyle = '#c65f38'; // attacker color
    ctx.fillRect(target.x, target.y - 70, 16, 6); // hit flash
    ctx.fillStyle = '#fff3dd';
    ctx.fillRect(target.x, target.y - 80, 8, 5); // particle effect
    target.hitFlash = .18;
    shake = Math.max(shake, a.dmg > 10 ? 11 : 6);
    freeze = a.dmg > 10 ? .07 : .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;
    ctx.fillStyle = '#c65f38';
    ctx.fillRect(target.x, GROUND + 20, 20, 20); // KO flash
    shake = Math.max(shake, 14);
    freeze = .075;
  }
}

// ---------- AI ----------
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;

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

  // guard if opponent is attacking & close
  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 {
    // in range: attack with some rhythm
    if (f._t > 0.35 + Math.random() * .4) {
      f._t = 0;
      if (dist < 100 && Math.random() < .55) tryAttack(f, 'heavy');
      else tryAttack(f, 'light');
    }
    mv = (Math.abs(dist - ideal) > 20 ? dir : 0);
  }

  f.vx += mv * 3.4;
}

// ---------- round logic ----------
function updateRound() {
  timeLeft -= dt;

  if (timeLeft <= 0 && !roundActive) {
    roundActive = true;
    setTimeout(() => {
      if (f1.hp > f2.hp) {
        resTitle.textContent = 'Pierre';
        resSub.textContent = `Gagne la manche ${roundNum} !`;
      } else if (f2.hp > f1.hp) {
        resTitle.textContent = 'Gaston';
        resSub.textContent = `Gagne la manche ${roundNum} !`;
      }
      resBtns.classList.remove('hide');
      roundActive = false;
    }, 500);
  }

  if (!roundActive && timeLeft < 3) {
    ctx.fillStyle = '#c65f38';
    ctx.fillRect(W / 2 - 100, H - 70, 200, 40);
    resKicker.textContent = 'Fin de la manche !';
    resTitle.classList.remove('hide');
    resSub.classList.remove('hide');
    resBtns.classList.remove('hide');
  }

  if (timeLeft < 3 && timeLeft > 1) {
    ctx.fillStyle = '#c65f38';
    ctx.fillRect(W / 2 - 100, H - 70, 200, 40);
    resKicker.textContent = 'Fin de la manche !';
    resTitle.classList.remove('hide');
    resSub.classList.remove('hide');
    resBtns.classList.remove('hide');
  }
}

// ---------- game loop ----------
const dt = .016;
setInterval(() => {
  if (state === 'fight') {
    updateFighter(f1, f2, dt);
    updateFighter(f2, f1, dt);
    updateRound();
  } else if (state === 'menu') {
    initClouds();
  }
}, 1000 / 60);

document.querySelector('.play[data-mode="1"]').addEventListener('click', () => startGame(1));
document.querySelector('.play[data-mode="2"]').addEventListener('click', () => startGame(2));
})();
</script>
</body>
</html>