Flux gravitationnel : jeu d'évasion en vol basé sur la physique
De Wikiprompt, l’encyclopédie libre de prompts
Flux gravitationnel : jeu d'évasion en vol basé sur la physique Invite détaillée pour générer un jeu HTML en fichier unique avec un gameplay basé sur la physique, des obstacles procéduraux en courbes de Bézier, une synthèse audio Web Audio, et une esthétique pastel zen.
Contenu du PromptEnregistrer
🌐
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gravity Flux · soft‑body flight</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #1a1c1e;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, sans-serif;
}
.canvas-wrapper {
background: #2d3136;
padding: 12px;
border-radius: 32px;
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
}
canvas {
display: block;
width: 800px;
height: 600px;
border-radius: 24px;
box-shadow: inset 0 0 30px rgba(0,0,0,0.5);
background: #f2ede7; /* pastel base */
}
.info {
display: flex;
justify-content: space-between;
margin-top: 12px;
color: #b9c2c9;
font-size: 14px;
letter-spacing: 1px;
padding: 0 8px;
}
.info span {
background: #2b2f33;
padding: 6px 14px;
border-radius: 40px;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
.btn {
background: #4e5a63;
border: none;
color: #eef2f5;
padding: 6px 16px;
border-radius: 40px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.btn:hover {
background: #6b7a85;
transform: scale(1.02);
}
</style>
</head>
<body>
<div>
<div class="canvas-wrapper">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="info">
<span>⚡ GRAVITY FLUX</span>
<span>✦ gates: <span id="gateCount">0</span></span>
<button class="btn" id="resetBtn">↻ reset</button>
</div>
</div>
</div>
<script>
(function() {
// ========== CANVAS & CONTEXT ==========
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const gateCountSpan = document.getElementById('gateCount');
// ========== GAME STATE ==========
let width = 800, height = 600;
let time = 0;
let passedGates = 0;
let gameOver = false;
// ========== SOFT-BODY SPHERE (self-inflating) ==========
const sphere = {
x: 400, y: 300,
baseRadius: 18,
currentRadius: 18,
vx: 0, vy: 0,
// soft body wobble (simulated via radius oscillation)
wobblePhase: 0,
// control inputs
up: false, down: false, left: false, right: false
};
// ========== GATE STRUCTURE ==========
// each gate: { x, y, w, h, phase, type, active }
// type: 'closing' or 'expanding' (organic morph)
let gates = [];
const GATE_COUNT = 6;
const GATE_MIN_W = 60;
const GATE_MAX_W = 180;
const GATE_HEIGHT = 220; // vertical opening (gap)
// ========== AUDIO (Web Audio API) ==========
let audioCtx = null;
let masterGain = null;
let ambientOsc = null;
let ambientGain = null;
function initAudio() {
if (audioCtx) return;
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.25;
masterGain.connect(audioCtx.destination);
// ambient pulse: low sine + slow LFO
ambientOsc = audioCtx.createOscillator();
ambientOsc.type = 'sine';
ambientOsc.frequency.value = 72; // deep pulse
ambientGain = audioCtx.createGain();
ambientGain.gain.value = 0.5;
const lfo = audioCtx.createOscillator();
lfo.frequency.value = 0.2;
const lfoGain = audioCtx.createGain();
lfoGain.gain.value = 0.3;
lfo.connect(lfoGain);
lfoGain.connect(ambientGain.gain);
ambientOsc.connect(ambientGain);
ambientGain.connect(masterGain);
ambientOsc.start();
lfo.start();
} catch(e) {
console.warn('Audio init failed', e);
}
}
// reactive acoustic sweep when passing gate
function playSweep() {
if (!audioCtx || !masterGain) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(180, now);
osc.frequency.exponentialRampToValueAtTime(720, now + 0.25);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
osc.connect(gain);
gain.connect(masterGain);
osc.start(now);
osc.stop(now + 0.45);
}
// ========== GATE GENERATION (procedural, morphing) ==========
function generateGates() {
gates = [];
for (let i = 0; i < GATE_COUNT; i++) {
const x = 120 + i * 120; // spread across width
const yBase = 200 + (i % 3) * 80; // vary vertical
const type = (i % 2 === 0) ? 'closing' : 'expanding';
gates.push({
x: x,
y: yBase,
w: 100, // base width
h: GATE_HEIGHT,
phase: i * 1.7, // phase offset for sine morph
type: type,
active: true,
// for organic bezier we store control points offsets
morphSpeed: 0.8 + (i * 0.12),
});
}
}
// ========== RESET ==========
function resetGame() {
passedGates = 0;
gameOver = false;
sphere.x = 400;
sphere.y = 300;
sphere.vx = 0;
sphere.vy = 0;
sphere.currentRadius = sphere.baseRadius;
generateGates();
updateGateCounter();
}
// ========== INPUT ==========
const keyState = {};
function handleKeyDown(e) {
if (e.key === 'ArrowUp' || e.key === 'w') { sphere.up = true; e.preventDefault(); }
if (e.key === 'ArrowDown' || e.key === 's') { sphere.down = true; e.preventDefault(); }
if (e.key === 'ArrowLeft' || e.key === 'a') { sphere.left = true; e.preventDefault(); }
if (e.key === 'ArrowRight' || e.key === 'd') { sphere.right = true; e.preventDefault(); }
}
function handleKeyUp(e) {
if (e.key === 'ArrowUp' || e.key === 'w') { sphere.up = false; e.preventDefault(); }
if (e.key === 'ArrowDown' || e.key === 's') { sphere.down = false; e.preventDefault(); }
if (e.key === 'ArrowLeft' || e.key === 'a') { sphere.left = false; e.preventDefault(); }
if (e.key === 'ArrowRight' || e.key === 'd') { sphere.right = false; e.preventDefault(); }
}
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
document.getElementById('resetBtn').addEventListener('click', () => {
resetGame();
initAudio(); // ensure audio on user gesture
});
// ========== PHYSICS & COLLISION ==========
function updateSphere(dt) {
// gentle acceleration
const accel = 0.6;
const damping = 0.92;
if (sphere.up) sphere.vy -= accel * dt * 60;
if (sphere.down) sphere.vy += accel * dt * 60;
if (sphere.left) sphere.vx -= accel * dt * 60;
if (sphere.right) sphere.vx += accel * dt * 60;
// apply damping
sphere.vx *= damping;
sphere.vy *= damping;
// integrate
sphere.x += sphere.vx * dt * 60;
sphere.y += sphere.vy * dt * 60;
// soft body self-inflate (radius pulsates)
sphere.wobblePhase += dt * 2.2;
sphere.currentRadius = sphere.baseRadius + Math.sin(sphere.wobblePhase) * 2.5;
// boundaries (soft)
if (sphere.x < 20) sphere.x = 20;
if (sphere.x > width - 20) sphere.x = width - 20;
if (sphere.y < 20) sphere.y = 20;
if (sphere.y > height - 20) sphere.y = height - 20;
}
// check collision with gate (organic shape approximated by rectangle + morph)
function checkGateCollision() {
const r = sphere.currentRadius;
for (let gate of gates) {
if (!gate.active) continue;
// compute current opening width based on sine morph
const t = time * gate.morphSpeed + gate.phase;
let wOffset;
if (gate.type === 'closing') {
// oscillates between 0.3 and 1.0 of base width
wOffset = 0.35 + 0.65 * (Math.sin(t) * 0.5 + 0.5);
} else {
// expanding: oscillates between 0.8 and 1.6
wOffset = 0.8 + 0.8 * (Math.sin(t) * 0.5 + 0.5);
}
const currentW = Math.max(30, gate.w * wOffset);
const halfW = currentW / 2;
const gateLeft = gate.x - halfW;
const gateRight = gate.x + halfW;
const gateTop = gate.y - gate.h / 2;
const gateBottom = gate.y + gate.h / 2;
// sphere AABB
const sLeft = sphere.x - r;
const sRight = sphere.x + r;
const sTop = sphere.y - r;
const sBottom = sphere.y + r;
// if sphere overlaps the gate region -> collision (unless it's a pass-through? but spec: no contact)
const overlapX = sRight > gateLeft && sLeft < gateRight;
const overlapY = sTop < gateBottom && sBottom > gateTop;
if (overlapX && overlapY) {
// but we need to check if sphere is inside the gap (vertical opening) -> actually the gate is the gap itself
// The gate is the opening: we must be inside the rectangle to pass.
// So we check if sphere is fully inside the gate area -> pass, else collision.
// But spec: "pass through dynamic structural gates without making contact" -> contact with the structure.
// The structure is the walls around the gap. So we treat the gate as the opening.
// If sphere is inside the opening, it's safe. If it touches the edges, it's collision.
// We'll check if sphere is fully inside the opening (with margin).
const margin = 2;
const insideX = (sphere.x - r > gateLeft + margin) && (sphere.x + r < gateRight - margin);
const insideY = (sphere.y - r > gateTop + margin) && (sphere.y + r < gateBottom - margin);
if (insideX && insideY) {
// safe zone: if previously not counted, count as pass
if (!gate.passed) {
gate.passed = true;
passedGates++;
updateGateCounter();
playSweep();
}
} else {
// contact with structure -> game over
gameOver = true;
return;
}
} else {
// not overlapping, reset passed flag if outside
gate.passed = false;
}
}
}
function updateGateCounter() {
gateCountSpan.textContent = passedGates;
}
// ========== RENDERING (zen pastel, soft shadows, bezier morph) ==========
function drawBackground() {
// soft gradient
const grad = ctx.createLinearGradient(0, 0, 0, height);
grad.addColorStop(0, '#f6efe8');
grad.addColorStop(1, '#e3d9cf');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, width, height);
}
// draw a single gate as organic bezier curves (morphing)
function drawGate(gate) {
const t = time * gate.morphSpeed + gate.phase;
// compute opening width
let wFactor;
if (gate.type === 'closing') {
wFactor = 0.35 + 0.65 * (Math.sin(t) * 0.5 + 0.5);
} else {
wFactor = 0.8 + 0.8 * (Math.sin(t) * 0.5 + 0.5);
}
const currentW = Math.max(30, gate.w * wFactor);
const halfW = currentW / 2;
const x = gate.x;
const yTop = gate.y - gate.h / 2;
const yBottom = gate.y + gate.h / 2;
// organic bezier: left and right edges are curved, top and bottom are wavy
const waveAmp = 12 * Math.sin(t * 1.3 + gate.phase);
const waveAmp2 = 10 * Math.cos(t * 0.9 + gate.phase * 2);
// left edge (bezier from top-left to bottom-left)
const leftX = x - halfW;
const rightX = x + halfW;
// draw as two filled shapes (left wall and right wall) with soft shadow
ctx.save();
ctx.shadowColor = 'rgba(80, 70, 60, 0.35)';
ctx.shadowBlur = 18;
ctx.shadowOffsetY = 6;
// left wall (organic shape)
ctx.beginPath();
ctx.moveTo(leftX, yTop);
// top curve
ctx.bezierCurveTo(leftX - waveAmp, yTop + 20, leftX + waveAmp2, yTop + 40, leftX, yTop + 60);
// inner edge (towards gap)
ctx.lineTo(leftX + 6, yTop + 60);
ctx.bezierCurveTo(leftX + 6 - waveAmp2, yTop + 80, leftX + 6 + waveAmp, yTop + 120, leftX + 6, yTop + 160);
ctx.lineTo(leftX + 6, yBottom - 20);
ctx.bezierCurveTo(leftX + 6 + waveAmp, yBottom - 40, leftX + 6 - waveAmp2, yBottom - 60, leftX + 6, yBottom);
// bottom edge
ctx.lineTo(leftX, yBottom);
ctx.bezierCurveTo(leftX - waveAmp2, yBottom - 20, leftX + waveAmp, yBottom - 40, leftX, yBottom - 60);
ctx.lineTo(leftX, yTop + 60);
ctx.bezierCurveTo(leftX + waveAmp, yTop + 40, leftX - waveAmp2, yTop + 20, leftX, yTop);
ctx.closePath();
ctx.fillStyle = '#cbbeb2';
ctx.fill();
// right wall (mirror)
ctx.beginPath();
ctx.moveTo(rightX, yTop);
ctx.bezierCurveTo(rightX + waveAmp, yTop + 20, rightX - waveAmp2, yTop + 40, rightX, yTop + 60);
ctx.lineTo(rightX - 6, yTop + 60);
ctx.bezierCurveTo(rightX - 6 + waveAmp2, yTop + 80, rightX - 6 - waveAmp, yTop + 120, rightX - 6, yTop + 160);
ctx.lineTo(rightX - 6, yBottom - 20);
ctx.bezierCurveTo(rightX - 6 - waveAmp, yBottom - 40, rightX - 6 + waveAmp2, yBottom - 60, rightX - 6, yBottom);
ctx.lineTo(rightX, yBottom);
ctx.bezierCurveTo(rightX + waveAmp2, yBottom - 20, rightX - waveAmp, yBottom - 40, rightX, yBottom - 60);
ctx.lineTo(rightX, yTop + 60);
ctx.bezierCurveTo(rightX - waveAmp, yTop + 40, rightX + waveAmp2, yTop + 20, rightX, yTop);
ctx.closePath();
ctx.fillStyle = '#cbbeb2';
ctx.fill();
// subtle top and bottom organic bars (to emphasize gap)
ctx.beginPath();
ctx.moveTo(leftX, yTop + 2);
ctx.bezierCurveTo(x - 20, yTop - 8, x + 20, yTop - 8, rightX, yTop + 2);
ctx.lineTo(rightX, yTop + 12);
ctx.bezierCurveTo(x + 20, yTop + 4, x - 20, yTop + 4, leftX, yTop + 12);
ctx.closePath();
ctx.fillStyle = '#b3a69a';
ctx.fill();
ctx.beginPath();
ctx.moveTo(leftX, yBottom - 2);
ctx.bezierCurveTo(x - 20, yBottom + 8, x + 20, yBottom + 8, rightX, yBottom - 2);
ctx.lineTo(rightX, yBottom - 12);
ctx.bezierCurveTo(x + 20, yBottom - 4, x - 20, yBottom - 4, leftX, yBottom - 12);
ctx.closePath();
ctx.fillStyle = '#b3a69a';
ctx.fill();
ctx.restore();
}
// draw soft-body sphere
function drawSphere() {
ctx.save();
ctx.shadowColor = 'rgba(60, 50, 40, 0.4)';
ctx.shadowBlur = 24;
ctx.shadowOffsetY = 8;
// radial gradient for soft volume
const grad = ctx.createRadialGradient(sphere.x-6, sphere.y-6, 4, sphere.x, sphere.y, sphere.currentRadius+4);
grad.addColorStop(0, '#faf6f2');
grad.addColorStop(0.7, '#e0d5cc');
grad.addColorStop(1, '#b8a99e');
ctx.beginPath();
ctx.arc(sphere.x, sphere.y, sphere.currentRadius, 0, Math.PI*2);
ctx.fillStyle = grad;
ctx.fill();
// inner highlight
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(sphere.x-3, sphere.y-3, sphere.currentRadius*0.3, 0, Math.PI*2);
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fill();
ctx.restore();
}
// ========== MAIN LOOP ==========
let lastTime = 0;
function gameLoop(timestamp) {
const dt = Math.min(0.05, (timestamp - lastTime) / 1000 || 0.016);
lastTime = timestamp;
time += dt;
if (!gameOver) {
updateSphere(dt);
checkGateCollision();
}
// render
ctx.clearRect(0, 0, width, height);
drawBackground();
// draw gates
for (let gate of gates) {
drawGate(gate);
}
// draw sphere (if not game over, else still draw but with dim)
if (gameOver) {
ctx.globalAlpha = 0.6;
drawSphere();
ctx.globalAlpha = 1.0;
// overlay message
ctx.font = 'bold 28px "Segoe UI", sans-serif';
ctx.fillStyle = '#5a4e46';
ctx.shadowColor = 'rgba(0,0,0,0.2)';
ctx.shadowBlur = 12;
ctx.textAlign = 'center';
ctx.fillText('contact · reset', width/2, height/2 - 20);
ctx.shadowBlur = 0;
} else {
drawSphere();
}
// small hint
ctx.font = '12px monospace';
ctx.fillStyle = '#7a6e64';
ctx.textAlign = 'left';
ctx.fillText('arrow keys / wasd', 20, height-16);
requestAnimationFrame(gameLoop);
}
// ========== INIT ==========
function init() {
resetGame();
// start audio on first interaction (user gesture)
const startAudio = () => {
initAudio();
window.removeEventListener('keydown', startAudio);
window.removeEventListener('click', startAudio);
};
window.addEventListener('keydown', startAudio);
window.addEventListener('click', startAudio);
requestAnimationFrame(gameLoop);
}
init();
})();
</script>
</body>
</html>
Connectez-vous pour voir le prompt complet
Continuer avec:
En vous connectant, vous acceptez nos Conditions et Confidentialité
Utilisation
Ce prompt est conçu pour être utilisé avec coding. Copiez le contenu ci-dessus et collez-le dans votre outil d’IA préféré.
Pour de meilleurs résultats, personnalisez les espaces réservés (indiqués par des crochets ou des majuscules) selon vos besoins.
Discussion
0 commentaires