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>Forest Jumper - Platformer 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: 10px;
|
|
color: white;
|
|
font-family: Arial, sans-serif;
|
|
font-size: 18px;
|
|
text-shadow: 2px 2px 4px #000;
|
|
z-index: 10;
|
|
}
|
|
#loading {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
|
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; }
|
|
.loading-bar {
|
|
width: 200px;
|
|
height: 8px;
|
|
background: #333;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.loading-bar-fill {
|
|
height: 100%;
|
|
background: #6366f1;
|
|
width: 0%;
|
|
transition: width 0.3s;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="loading">
|
|
<h1>Forest Jumper</h1>
|
|
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
|
|
<p id="loadingText">Loading assets...</p>
|
|
</div>
|
|
<canvas id="gameCanvas"></canvas>
|
|
<div id="ui">
|
|
<div>Score: <span id="score">0</span></div>
|
|
<div>Coins: <span id="coins">0</span></div>
|
|
</div>
|
|
|
|
<!-- Asset Libraries -->
|
|
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
|
<script src="/assets/backgrounds/effects.js"></script>
|
|
<script src="/assets/backgrounds/themes/nature.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/pure.js"></script>
|
|
<script src="/assets/avatar/contextRenderer.js"></script>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// PLATFORMER EXAMPLE - Full Body Avatar Demo
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// Demonstrates: FULL_BODY avatar, platformer-standard context,
|
|
// nature/forest background, Playful music mood
|
|
|
|
const GAME_CONFIG = {
|
|
title: "Forest Jumper",
|
|
assets: {
|
|
music: { mood: "Playful", energy: 6, enabled: true },
|
|
background: { theme: "nature", variant: "forest", animated: true },
|
|
avatarContext: { mode: "FULL_BODY", context: "platformer-standard", showAccessories: true }
|
|
},
|
|
gameplay: {
|
|
gravity: 1800,
|
|
jumpForce: 650,
|
|
moveSpeed: 280,
|
|
platformCount: 8,
|
|
coinSpawnRate: 2000
|
|
}
|
|
};
|
|
|
|
// Player avatar configuration
|
|
const playerAvatar = {
|
|
faceShape: 'round',
|
|
skinTone: 'medium',
|
|
hairStyle: 'short',
|
|
hairColor: '#3d2314',
|
|
eyeShape: 'round',
|
|
eyeColor: '#4a7c59',
|
|
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 = {
|
|
playerX: 100,
|
|
playerY: 300,
|
|
playerVelX: 0,
|
|
playerVelY: 0,
|
|
isGrounded: false,
|
|
facingRight: true,
|
|
currentAnim: 'idle',
|
|
animTime: 0,
|
|
|
|
score: 0,
|
|
coins: 0,
|
|
gameOver: false,
|
|
paused: false,
|
|
lastTime: 0,
|
|
|
|
platforms: [],
|
|
collectedCoins: [],
|
|
particles: []
|
|
};
|
|
|
|
const keys = {};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// LOADING
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
let assetsLoaded = false;
|
|
|
|
function updateLoading(progress, text) {
|
|
document.getElementById('loadingFill').style.width = progress + '%';
|
|
document.getElementById('loadingText').textContent = text;
|
|
}
|
|
|
|
function hideLoading() {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
|
|
async function loadAssets() {
|
|
updateLoading(20, 'Loading backgrounds...');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
updateLoading(40, 'Loading music...');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
updateLoading(60, 'Loading avatars...');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
updateLoading(80, 'Initializing game...');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
|
|
updateLoading(100, 'Ready!');
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
assetsLoaded = true;
|
|
hideLoading();
|
|
initGame();
|
|
state.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
|
|
// 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) { console.log('Music not available'); }
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME LOGIC
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function initGame() {
|
|
state.playerX = 100;
|
|
state.playerY = canvas.height - 200;
|
|
|
|
// Generate platforms
|
|
state.platforms = [];
|
|
const groundY = canvas.height - 50;
|
|
|
|
// Ground
|
|
state.platforms.push({ x: 0, y: groundY, w: canvas.width, h: 50, isGround: true });
|
|
|
|
// Floating platforms
|
|
for (let i = 0; i < GAME_CONFIG.gameplay.platformCount; i++) {
|
|
state.platforms.push({
|
|
x: 100 + i * (canvas.width / GAME_CONFIG.gameplay.platformCount),
|
|
y: groundY - 100 - Math.random() * 250,
|
|
w: 100 + Math.random() * 80,
|
|
h: 20
|
|
});
|
|
}
|
|
|
|
// Add coins on platforms
|
|
state.collectedCoins = [];
|
|
state.platforms.forEach((p, i) => {
|
|
if (!p.isGround && Math.random() > 0.3) {
|
|
state.collectedCoins.push({
|
|
x: p.x + p.w / 2,
|
|
y: p.y - 30,
|
|
collected: false,
|
|
bounce: Math.random() * Math.PI * 2
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateGame(dt) {
|
|
const { gravity, jumpForce, moveSpeed } = GAME_CONFIG.gameplay;
|
|
|
|
// Horizontal movement
|
|
if (keys['ArrowLeft'] || keys['KeyA']) {
|
|
state.playerVelX = -moveSpeed;
|
|
state.facingRight = false;
|
|
if (state.isGrounded) state.currentAnim = 'walk';
|
|
} else if (keys['ArrowRight'] || keys['KeyD']) {
|
|
state.playerVelX = moveSpeed;
|
|
state.facingRight = true;
|
|
if (state.isGrounded) state.currentAnim = 'walk';
|
|
} else {
|
|
state.playerVelX *= 0.85;
|
|
if (state.isGrounded && Math.abs(state.playerVelX) < 10) {
|
|
state.currentAnim = 'idle';
|
|
}
|
|
}
|
|
|
|
// Jump
|
|
if ((keys['Space'] || keys['ArrowUp'] || keys['KeyW']) && state.isGrounded) {
|
|
state.playerVelY = -jumpForce;
|
|
state.isGrounded = false;
|
|
state.currentAnim = 'jump';
|
|
}
|
|
|
|
// Apply gravity
|
|
state.playerVelY += gravity * dt;
|
|
|
|
// Move player
|
|
state.playerX += state.playerVelX * dt;
|
|
state.playerY += state.playerVelY * dt;
|
|
|
|
// Platform collision
|
|
state.isGrounded = false;
|
|
const playerRect = {
|
|
x: state.playerX - 20,
|
|
y: state.playerY - 80,
|
|
w: 40,
|
|
h: 80
|
|
};
|
|
|
|
state.platforms.forEach(p => {
|
|
// Check if player is above platform and falling
|
|
if (state.playerVelY >= 0 &&
|
|
playerRect.x + playerRect.w > p.x &&
|
|
playerRect.x < p.x + p.w &&
|
|
playerRect.y + playerRect.h >= p.y &&
|
|
playerRect.y + playerRect.h <= p.y + p.h + state.playerVelY * dt + 5) {
|
|
|
|
state.playerY = p.y - 80;
|
|
state.playerVelY = 0;
|
|
state.isGrounded = true;
|
|
if (state.currentAnim === 'jump') state.currentAnim = 'idle';
|
|
}
|
|
});
|
|
|
|
// Animation when in air
|
|
if (!state.isGrounded) {
|
|
state.currentAnim = state.playerVelY < 0 ? 'jump' : 'jump';
|
|
}
|
|
|
|
// Screen bounds
|
|
if (state.playerX < 30) state.playerX = 30;
|
|
if (state.playerX > canvas.width - 30) state.playerX = canvas.width - 30;
|
|
|
|
// Fall death
|
|
if (state.playerY > canvas.height + 100) {
|
|
endGame();
|
|
}
|
|
|
|
// Coin collection
|
|
state.collectedCoins.forEach(coin => {
|
|
if (!coin.collected) {
|
|
coin.bounce += dt * 3;
|
|
const coinY = coin.y + Math.sin(coin.bounce) * 5;
|
|
const dx = state.playerX - coin.x;
|
|
const dy = (state.playerY - 40) - coinY;
|
|
if (Math.sqrt(dx*dx + dy*dy) < 35) {
|
|
coin.collected = true;
|
|
state.coins++;
|
|
state.score += 10;
|
|
updateScore(0);
|
|
|
|
// Particle effect
|
|
for (let i = 0; i < 8; i++) {
|
|
state.particles.push({
|
|
x: coin.x,
|
|
y: coinY,
|
|
vx: (Math.random() - 0.5) * 200,
|
|
vy: -Math.random() * 150 - 50,
|
|
life: 1,
|
|
color: '#FFD700'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update particles
|
|
state.particles = state.particles.filter(p => {
|
|
p.x += p.vx * dt;
|
|
p.y += p.vy * dt;
|
|
p.vy += 400 * dt;
|
|
p.life -= dt * 2;
|
|
return p.life > 0;
|
|
});
|
|
|
|
// Score over time
|
|
state.score += Math.floor(dt * 5);
|
|
updateScore(0);
|
|
|
|
state.animTime += dt * 1000;
|
|
}
|
|
|
|
function renderGame() {
|
|
// Background
|
|
if (window.BackgroundEngine) {
|
|
window.BackgroundEngine.render(
|
|
ctx,
|
|
GAME_CONFIG.assets.background.theme,
|
|
GAME_CONFIG.assets.background.variant,
|
|
canvas.width,
|
|
canvas.height,
|
|
state.animTime
|
|
);
|
|
} else {
|
|
ctx.fillStyle = '#228B22';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
// Platforms
|
|
state.platforms.forEach(p => {
|
|
if (p.isGround) {
|
|
ctx.fillStyle = '#3d2817';
|
|
ctx.fillRect(p.x, p.y, p.w, p.h);
|
|
ctx.fillStyle = '#4a7c59';
|
|
ctx.fillRect(p.x, p.y, p.w, 10);
|
|
} else {
|
|
// Floating platform
|
|
ctx.fillStyle = '#5d4037';
|
|
ctx.fillRect(p.x, p.y, p.w, p.h);
|
|
ctx.fillStyle = '#795548';
|
|
ctx.fillRect(p.x + 2, p.y + 2, p.w - 4, 6);
|
|
}
|
|
});
|
|
|
|
// Coins
|
|
state.collectedCoins.forEach(coin => {
|
|
if (!coin.collected) {
|
|
const coinY = coin.y + Math.sin(coin.bounce) * 5;
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.beginPath();
|
|
ctx.arc(coin.x, coinY, 12, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#FFA500';
|
|
ctx.beginPath();
|
|
ctx.arc(coin.x - 3, coinY - 3, 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
});
|
|
|
|
// Particles
|
|
state.particles.forEach(p => {
|
|
ctx.globalAlpha = p.life;
|
|
ctx.fillStyle = p.color;
|
|
ctx.fillRect(p.x - 3, p.y - 3, 6, 6);
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
|
|
// Player avatar
|
|
ctx.save();
|
|
ctx.translate(state.playerX, state.playerY);
|
|
if (!state.facingRight) {
|
|
ctx.scale(-1, 1);
|
|
}
|
|
ctx.translate(-32, -160);
|
|
|
|
if (window.AvatarSystem) {
|
|
window.AvatarSystem.render(
|
|
ctx,
|
|
playerAvatar,
|
|
GAME_CONFIG.assets.avatarContext.mode,
|
|
state.currentAnim,
|
|
state.animTime
|
|
);
|
|
} else {
|
|
// Fallback
|
|
ctx.fillStyle = '#4CAF50';
|
|
ctx.fillRect(12, 100, 40, 60);
|
|
ctx.fillStyle = '#FFCC80';
|
|
ctx.beginPath();
|
|
ctx.arc(32, 80, 25, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// UTILITIES
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function updateScore(points) {
|
|
document.getElementById('score').textContent = state.score;
|
|
document.getElementById('coins').textContent = state.coins;
|
|
}
|
|
|
|
function endGame() {
|
|
state.gameOver = true;
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
|
}
|
|
setTimeout(() => {
|
|
if (confirm(`Game Over!\nScore: ${state.score}\nCoins: ${state.coins}\n\nPlay again?`)) {
|
|
state.score = 0;
|
|
state.coins = 0;
|
|
state.gameOver = false;
|
|
initGame();
|
|
state.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// INPUT
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
document.addEventListener('keydown', e => {
|
|
keys[e.code] = true;
|
|
if (e.code === 'Escape') {
|
|
state.paused = !state.paused;
|
|
if (!state.paused) {
|
|
state.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', e => {
|
|
keys[e.code] = false;
|
|
});
|
|
|
|
// Touch controls
|
|
let touchStartX = 0;
|
|
canvas.addEventListener('touchstart', e => {
|
|
e.preventDefault();
|
|
touchStartX = e.touches[0].clientX;
|
|
const y = e.touches[0].clientY;
|
|
|
|
// Top half = jump
|
|
if (y < canvas.height / 2) {
|
|
keys['Space'] = true;
|
|
}
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchmove', e => {
|
|
e.preventDefault();
|
|
const x = e.touches[0].clientX;
|
|
const dx = x - touchStartX;
|
|
|
|
keys['ArrowLeft'] = dx < -30;
|
|
keys['ArrowRight'] = dx > 30;
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchend', e => {
|
|
keys['ArrowLeft'] = false;
|
|
keys['ArrowRight'] = false;
|
|
keys['Space'] = false;
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME LOOP
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function gameLoop(time) {
|
|
if (state.gameOver || state.paused) return;
|
|
|
|
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
|
|
state.lastTime = time;
|
|
|
|
updateGame(dt);
|
|
renderGame();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Parent window controls
|
|
window.pauseGame = () => { state.paused = true; };
|
|
window.resumeGame = () => {
|
|
state.paused = false;
|
|
state.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
};
|
|
|
|
// Start loading
|
|
loadAssets();
|
|
</script>
|
|
</body>
|
|
</html>
|