534 lines
19 KiB
HTML
534 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Sky Glide - Flappy Example</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; overflow: hidden; background: #1a1a2e; touch-action: none; }
|
|
canvas { display: block; }
|
|
#ui {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
color: white;
|
|
font-family: Arial, sans-serif;
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
text-shadow: 2px 2px 4px #000;
|
|
z-index: 10;
|
|
}
|
|
#loading {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: linear-gradient(135deg, #87CEEB, #4682B4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-family: Arial, sans-serif;
|
|
z-index: 100;
|
|
}
|
|
#loading h1 { margin-bottom: 1rem; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
|
|
.loading-bar {
|
|
width: 200px;
|
|
height: 8px;
|
|
background: rgba(255,255,255,0.3);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.loading-bar-fill {
|
|
height: 100%;
|
|
background: white;
|
|
width: 0%;
|
|
transition: width 0.3s;
|
|
}
|
|
#startScreen {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
display: none;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-family: Arial, sans-serif;
|
|
z-index: 50;
|
|
}
|
|
#startScreen h2 { font-size: 2rem; margin-bottom: 1rem; }
|
|
#startScreen p { font-size: 1.25rem; opacity: 0.9; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="loading">
|
|
<h1>Sky Glide</h1>
|
|
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
|
|
<p id="loadingText">Loading assets...</p>
|
|
</div>
|
|
<div id="startScreen">
|
|
<h2>Tap or Press Space to Start</h2>
|
|
<p>Avoid the pipes!</p>
|
|
</div>
|
|
<canvas id="gameCanvas"></canvas>
|
|
<div id="ui"><span id="score">0</span></div>
|
|
|
|
<!-- Asset Libraries -->
|
|
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
|
<script src="/assets/backgrounds/effects.js"></script>
|
|
<script src="/assets/backgrounds/themes/sky.js"></script>
|
|
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
|
<script src="/assets/music/moodCategories.js"></script>
|
|
<script src="/assets/music/musicEngine.js"></script>
|
|
<script src="/assets/music/library/songs1-80.js"></script>
|
|
<script src="/assets/avatar/avatarData.js"></script>
|
|
<script src="/assets/avatar/accessories.js"></script>
|
|
<script src="/assets/avatar/animations.js"></script>
|
|
<script src="/assets/avatar/avatarRenderer.js"></script>
|
|
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
|
<script src="/assets/avatar/avatarSystem.js"></script>
|
|
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
|
<script src="/assets/avatar/contextRenderer.js"></script>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// FLAPPY EXAMPLE - Head Only Avatar Demo
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// Demonstrates: HEAD_ONLY avatar, flappy-style context,
|
|
// sky/clouds background, Playful music mood
|
|
|
|
const GAME_CONFIG = {
|
|
title: "Sky Glide",
|
|
assets: {
|
|
music: { mood: "Playful", energy: 5, enabled: true },
|
|
background: { theme: "sky", variant: "clouds", animated: true },
|
|
avatarContext: { mode: "HEAD_ONLY", context: "flappy-style", showAccessories: true }
|
|
},
|
|
gameplay: {
|
|
gravity: 1200,
|
|
flapForce: 400,
|
|
pipeSpeed: 200,
|
|
pipeGap: 180,
|
|
pipeWidth: 70,
|
|
pipeSpawnRate: 1800
|
|
}
|
|
};
|
|
|
|
const playerAvatar = {
|
|
faceShape: 'round',
|
|
skinTone: 'light',
|
|
hairStyle: 'spiky',
|
|
hairColor: '#FFD700',
|
|
eyeShape: 'round',
|
|
eyeColor: '#3498db',
|
|
noseShape: 'small',
|
|
mouthShape: 'smile'
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// SETUP
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
let state = {
|
|
playerY: 0,
|
|
playerVelY: 0,
|
|
playerRotation: 0,
|
|
animTime: 0,
|
|
|
|
score: 0,
|
|
highScore: parseInt(localStorage.getItem('skyglide_high') || '0'),
|
|
gameOver: false,
|
|
started: false,
|
|
paused: false,
|
|
lastTime: 0,
|
|
|
|
pipes: [],
|
|
lastPipeTime: 0,
|
|
clouds: []
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// LOADING
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function updateLoading(progress, text) {
|
|
document.getElementById('loadingFill').style.width = progress + '%';
|
|
document.getElementById('loadingText').textContent = text;
|
|
}
|
|
|
|
function hideLoading() {
|
|
document.getElementById('loading').style.display = 'none';
|
|
document.getElementById('startScreen').style.display = 'flex';
|
|
}
|
|
|
|
async function loadAssets() {
|
|
updateLoading(25, 'Loading sky...');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
updateLoading(50, 'Loading music...');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
updateLoading(75, 'Loading avatar...');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
updateLoading(100, 'Ready!');
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
hideLoading();
|
|
initGame();
|
|
state.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME LOGIC
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function initGame() {
|
|
state.playerY = canvas.height / 2;
|
|
state.playerVelY = 0;
|
|
state.playerRotation = 0;
|
|
state.pipes = [];
|
|
state.lastPipeTime = 0;
|
|
state.score = 0;
|
|
state.gameOver = false;
|
|
updateScoreDisplay();
|
|
|
|
// Initialize clouds for parallax
|
|
state.clouds = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
state.clouds.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height * 0.6,
|
|
size: 50 + Math.random() * 100,
|
|
speed: 20 + Math.random() * 30
|
|
});
|
|
}
|
|
}
|
|
|
|
function flap() {
|
|
if (state.gameOver) {
|
|
state.score = 0;
|
|
state.gameOver = false;
|
|
state.started = false;
|
|
document.getElementById('startScreen').style.display = 'flex';
|
|
initGame();
|
|
return;
|
|
}
|
|
|
|
if (!state.started) {
|
|
state.started = true;
|
|
document.getElementById('startScreen').style.display = 'none';
|
|
|
|
// Start music
|
|
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
|
|
try {
|
|
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
|
|
} catch(e) {}
|
|
}
|
|
}
|
|
|
|
state.playerVelY = -GAME_CONFIG.gameplay.flapForce;
|
|
}
|
|
|
|
function updateGame(dt) {
|
|
if (!state.started || state.gameOver) {
|
|
// Bob animation when not playing
|
|
state.playerY = canvas.height / 2 + Math.sin(state.animTime / 300) * 20;
|
|
state.animTime += dt * 1000;
|
|
return;
|
|
}
|
|
|
|
const { gravity, pipeSpeed, pipeGap, pipeWidth, pipeSpawnRate } = GAME_CONFIG.gameplay;
|
|
|
|
// Apply gravity
|
|
state.playerVelY += gravity * dt;
|
|
state.playerY += state.playerVelY * dt;
|
|
|
|
// Rotation based on velocity
|
|
state.playerRotation = Math.min(Math.max(state.playerVelY / 10, -30), 90);
|
|
|
|
// Spawn pipes
|
|
state.lastPipeTime += dt * 1000;
|
|
if (state.lastPipeTime > pipeSpawnRate) {
|
|
state.lastPipeTime = 0;
|
|
const gapY = 150 + Math.random() * (canvas.height - 350);
|
|
|
|
state.pipes.push({
|
|
x: canvas.width + pipeWidth,
|
|
gapY: gapY,
|
|
passed: false
|
|
});
|
|
}
|
|
|
|
// Update pipes
|
|
state.pipes = state.pipes.filter(pipe => {
|
|
pipe.x -= pipeSpeed * dt;
|
|
|
|
// Score when passing
|
|
if (!pipe.passed && pipe.x + pipeWidth < canvas.width / 3) {
|
|
pipe.passed = true;
|
|
state.score++;
|
|
updateScoreDisplay();
|
|
}
|
|
|
|
return pipe.x > -pipeWidth;
|
|
});
|
|
|
|
// Update clouds
|
|
state.clouds.forEach(cloud => {
|
|
cloud.x -= cloud.speed * dt;
|
|
if (cloud.x + cloud.size < 0) {
|
|
cloud.x = canvas.width + cloud.size;
|
|
cloud.y = Math.random() * canvas.height * 0.6;
|
|
}
|
|
});
|
|
|
|
// Collision detection
|
|
const playerX = canvas.width / 3;
|
|
const playerSize = 32;
|
|
|
|
// Ground/ceiling
|
|
if (state.playerY < playerSize || state.playerY > canvas.height - playerSize) {
|
|
endGame();
|
|
return;
|
|
}
|
|
|
|
// Pipes
|
|
state.pipes.forEach(pipe => {
|
|
const pipeLeft = pipe.x;
|
|
const pipeRight = pipe.x + pipeWidth;
|
|
|
|
if (playerX + playerSize > pipeLeft && playerX - playerSize < pipeRight) {
|
|
// Check top pipe
|
|
if (state.playerY - playerSize < pipe.gapY - pipeGap / 2) {
|
|
endGame();
|
|
}
|
|
// Check bottom pipe
|
|
if (state.playerY + playerSize > pipe.gapY + pipeGap / 2) {
|
|
endGame();
|
|
}
|
|
}
|
|
});
|
|
|
|
state.animTime += dt * 1000;
|
|
}
|
|
|
|
function renderGame() {
|
|
// Sky gradient background
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
|
gradient.addColorStop(0, '#87CEEB');
|
|
gradient.addColorStop(0.5, '#B0E0E6');
|
|
gradient.addColorStop(1, '#E0F7FA');
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Clouds (parallax)
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
state.clouds.forEach(cloud => {
|
|
drawCloud(cloud.x, cloud.y, cloud.size);
|
|
});
|
|
|
|
// Pipes
|
|
state.pipes.forEach(pipe => {
|
|
const { pipeWidth, pipeGap } = GAME_CONFIG.gameplay;
|
|
const topHeight = pipe.gapY - pipeGap / 2;
|
|
const bottomY = pipe.gapY + pipeGap / 2;
|
|
|
|
// Top pipe
|
|
ctx.fillStyle = '#2ECC71';
|
|
ctx.fillRect(pipe.x, 0, pipeWidth, topHeight);
|
|
ctx.fillStyle = '#27AE60';
|
|
ctx.fillRect(pipe.x - 5, topHeight - 30, pipeWidth + 10, 30);
|
|
|
|
// Bottom pipe
|
|
ctx.fillStyle = '#2ECC71';
|
|
ctx.fillRect(pipe.x, bottomY, pipeWidth, canvas.height - bottomY);
|
|
ctx.fillStyle = '#27AE60';
|
|
ctx.fillRect(pipe.x - 5, bottomY, pipeWidth + 10, 30);
|
|
|
|
// Pipe highlights
|
|
ctx.fillStyle = '#58D68D';
|
|
ctx.fillRect(pipe.x + 5, 0, 10, topHeight - 30);
|
|
ctx.fillRect(pipe.x + 5, bottomY + 30, 10, canvas.height - bottomY - 30);
|
|
});
|
|
|
|
// Ground
|
|
ctx.fillStyle = '#DEB887';
|
|
ctx.fillRect(0, canvas.height - 30, canvas.width, 30);
|
|
ctx.fillStyle = '#8B4513';
|
|
ctx.fillRect(0, canvas.height - 35, canvas.width, 5);
|
|
|
|
// Player
|
|
const playerX = canvas.width / 3;
|
|
|
|
ctx.save();
|
|
ctx.translate(playerX, state.playerY);
|
|
ctx.rotate(state.playerRotation * Math.PI / 180);
|
|
|
|
if (window.ContextRenderer) {
|
|
// Use context renderer for flappy style
|
|
ctx.translate(-40, -40);
|
|
window.ContextRenderer.render(
|
|
ctx,
|
|
GAME_CONFIG.assets.avatarContext.context,
|
|
playerAvatar,
|
|
40, 40,
|
|
'idle',
|
|
state.animTime
|
|
);
|
|
} else if (window.AvatarSystem) {
|
|
ctx.translate(-32, -32);
|
|
window.AvatarSystem.render(
|
|
ctx,
|
|
playerAvatar,
|
|
'HEAD_ONLY',
|
|
'idle',
|
|
state.animTime
|
|
);
|
|
} else {
|
|
// Fallback bird
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, 30, 25, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Eye
|
|
ctx.fillStyle = 'white';
|
|
ctx.beginPath();
|
|
ctx.arc(10, -5, 10, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = 'black';
|
|
ctx.beginPath();
|
|
ctx.arc(12, -5, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Beak
|
|
ctx.fillStyle = '#FF6B00';
|
|
ctx.beginPath();
|
|
ctx.moveTo(25, 0);
|
|
ctx.lineTo(40, 5);
|
|
ctx.lineTo(25, 10);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Wing
|
|
ctx.fillStyle = '#FFA500';
|
|
ctx.beginPath();
|
|
ctx.ellipse(-5, 5, 15, 10, Math.sin(state.animTime / 50) * 0.3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
|
|
// Game over overlay
|
|
if (state.gameOver) {
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
ctx.fillStyle = 'white';
|
|
ctx.font = 'bold 48px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('Game Over', canvas.width / 2, canvas.height / 2 - 60);
|
|
|
|
ctx.font = '32px Arial';
|
|
ctx.fillText(`Score: ${state.score}`, canvas.width / 2, canvas.height / 2);
|
|
ctx.fillText(`Best: ${state.highScore}`, canvas.width / 2, canvas.height / 2 + 40);
|
|
|
|
ctx.font = '24px Arial';
|
|
ctx.fillText('Tap to Restart', canvas.width / 2, canvas.height / 2 + 100);
|
|
}
|
|
}
|
|
|
|
function drawCloud(x, y, size) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, size * 0.5, 0, Math.PI * 2);
|
|
ctx.arc(x + size * 0.35, y - size * 0.15, size * 0.4, 0, Math.PI * 2);
|
|
ctx.arc(x + size * 0.6, y, size * 0.35, 0, Math.PI * 2);
|
|
ctx.arc(x + size * 0.3, y + size * 0.1, size * 0.3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// UTILITIES
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function updateScoreDisplay() {
|
|
document.getElementById('score').textContent = state.score;
|
|
}
|
|
|
|
function endGame() {
|
|
state.gameOver = true;
|
|
if (state.score > state.highScore) {
|
|
state.highScore = state.score;
|
|
localStorage.setItem('skyglide_high', state.highScore);
|
|
}
|
|
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// INPUT
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if (e.code === 'Space') {
|
|
e.preventDefault();
|
|
flap();
|
|
}
|
|
if (e.code === 'Escape') {
|
|
state.paused = !state.paused;
|
|
if (!state.paused) {
|
|
state.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('click', flap);
|
|
canvas.addEventListener('touchstart', e => {
|
|
e.preventDefault();
|
|
flap();
|
|
}, { passive: false });
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME LOOP
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function gameLoop(time) {
|
|
if (state.paused) return;
|
|
|
|
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
|
|
state.lastTime = time;
|
|
|
|
updateGame(dt);
|
|
renderGame();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
window.pauseGame = () => { state.paused = true; };
|
|
window.resumeGame = () => {
|
|
state.paused = false;
|
|
state.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
};
|
|
|
|
loadAssets();
|
|
</script>
|
|
</body>
|
|
</html>
|