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