← 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>