Initial commit
This commit is contained in:
@@ -0,0 +1,675 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Asset Comparison Demo - Same Game, Different Assets</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f0f1a;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #1a1a2e;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.game-panel {
|
||||
background: #1a1a2e;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 1.125rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #6366f1;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge.space { background: #3b82f6; }
|
||||
.badge.nature { background: #22c55e; }
|
||||
.badge.spooky { background: #8b5cf6; }
|
||||
.badge.retro { background: #f59e0b; }
|
||||
|
||||
.game-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/10;
|
||||
background: #0f0f1a;
|
||||
}
|
||||
|
||||
.game-frame canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #334155;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #252542;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #1a1a2e;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.controls h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #818cf8;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.btn.secondary:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.code-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.code-section h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.code-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.code-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #252542;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-tab.active {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #0f0f1a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.75rem;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
opacity: 0.9;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<h1>Asset Comparison Demo</h1>
|
||||
<p>The same dodge-the-obstacles game rendered with 4 different asset combinations</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="grid">
|
||||
<!-- Space Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Space Dodger</span></h2>
|
||||
<span class="badge space">Space Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game1" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">space/nebula</span>
|
||||
<span class="tag">Epic music</span>
|
||||
<span class="tag">spaceship-cockpit</span>
|
||||
<span class="tag">HEAD_ONLY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nature Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Forest Runner</span></h2>
|
||||
<span class="badge nature">Nature Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game2" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">nature/forest</span>
|
||||
<span class="tag">Playful music</span>
|
||||
<span class="tag">platformer-standard</span>
|
||||
<span class="tag">FULL_BODY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spooky Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Haunted Escape</span></h2>
|
||||
<span class="badge spooky">Spooky Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game3" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">spooky/hauntedHouse</span>
|
||||
<span class="tag">Mysterious music</span>
|
||||
<span class="tag">adventure-hero</span>
|
||||
<span class="tag">FULL_BODY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retro Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Pixel Dash</span></h2>
|
||||
<span class="badge retro">Retro Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game4" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">retro/synthwave</span>
|
||||
<span class="tag">Quirky music</span>
|
||||
<span class="tag">runner</span>
|
||||
<span class="tag">FULL_BODY</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<h3>Global Controls</h3>
|
||||
<div class="control-row">
|
||||
<button class="btn" onclick="startAll()">Start All Games</button>
|
||||
<button class="btn secondary" onclick="pauseAll()">Pause All</button>
|
||||
<button class="btn secondary" onclick="resetAll()">Reset All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>How to Play</h3>
|
||||
<p>
|
||||
Use <strong>Arrow Keys</strong> or <strong>WASD</strong> to move. Avoid the obstacles!
|
||||
Each panel shows the same game logic with different visual assets.
|
||||
Notice how the background, music mood, avatar context, and rendering mode
|
||||
completely change the feel of the game while the core mechanics remain identical.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="code-section">
|
||||
<h3>Configuration Code</h3>
|
||||
<div class="code-tabs">
|
||||
<button class="code-tab active" onclick="showCode('space')">Space</button>
|
||||
<button class="code-tab" onclick="showCode('nature')">Nature</button>
|
||||
<button class="code-tab" onclick="showCode('spooky')">Spooky</button>
|
||||
<button class="code-tab" onclick="showCode('retro')">Retro</button>
|
||||
</div>
|
||||
<div class="code-block" id="codeDisplay">const GAME_CONFIG = {
|
||||
title: "Space Dodger",
|
||||
assets: {
|
||||
music: { mood: "Epic", energy: 8, enabled: true },
|
||||
background: { theme: "space", variant: "nebula", animated: true },
|
||||
avatarContext: {
|
||||
mode: "HEAD_ONLY",
|
||||
context: "spaceship-cockpit",
|
||||
showAccessories: true
|
||||
}
|
||||
}
|
||||
};</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Libraries -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/space.js"></script>
|
||||
<script src="/assets/backgrounds/themes/nature.js"></script>
|
||||
<script src="/assets/backgrounds/themes/spooky.js"></script>
|
||||
<script src="/assets/backgrounds/themes/retro.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.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/contexts/pure.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// COMPARISON DEMO - Same game, 4 different asset sets
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const CONFIGS = {
|
||||
space: {
|
||||
title: "Space Dodger",
|
||||
assets: {
|
||||
music: { mood: "Epic", energy: 8 },
|
||||
background: { theme: "space", variant: "nebula" },
|
||||
avatar: { mode: "HEAD_ONLY", context: "spaceship-cockpit" }
|
||||
},
|
||||
colors: {
|
||||
player: '#3b82f6',
|
||||
obstacle: '#ef4444',
|
||||
coin: '#fbbf24'
|
||||
}
|
||||
},
|
||||
nature: {
|
||||
title: "Forest Runner",
|
||||
assets: {
|
||||
music: { mood: "Playful", energy: 6 },
|
||||
background: { theme: "nature", variant: "forest" },
|
||||
avatar: { mode: "FULL_BODY", context: "platformer-standard" }
|
||||
},
|
||||
colors: {
|
||||
player: '#22c55e',
|
||||
obstacle: '#a16207',
|
||||
coin: '#fbbf24'
|
||||
}
|
||||
},
|
||||
spooky: {
|
||||
title: "Haunted Escape",
|
||||
assets: {
|
||||
music: { mood: "Mysterious", energy: 5 },
|
||||
background: { theme: "spooky", variant: "hauntedHouse" },
|
||||
avatar: { mode: "FULL_BODY", context: "adventure-hero" }
|
||||
},
|
||||
colors: {
|
||||
player: '#a855f7',
|
||||
obstacle: '#7c3aed',
|
||||
coin: '#c084fc'
|
||||
}
|
||||
},
|
||||
retro: {
|
||||
title: "Pixel Dash",
|
||||
assets: {
|
||||
music: { mood: "Quirky", energy: 7 },
|
||||
background: { theme: "retro", variant: "synthwave" },
|
||||
avatar: { mode: "FULL_BODY", context: "runner" }
|
||||
},
|
||||
colors: {
|
||||
player: '#f59e0b',
|
||||
obstacle: '#ec4899',
|
||||
coin: '#06b6d4'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const playerAvatar = {
|
||||
faceShape: 'round',
|
||||
skinTone: 'medium',
|
||||
hairStyle: 'short',
|
||||
hairColor: '#3d2314',
|
||||
eyeShape: 'round',
|
||||
eyeColor: '#4a7c59'
|
||||
};
|
||||
|
||||
// Game instances
|
||||
const games = {};
|
||||
const keys = {};
|
||||
|
||||
// Input handling
|
||||
document.addEventListener('keydown', e => { keys[e.code] = true; });
|
||||
document.addEventListener('keyup', e => { keys[e.code] = false; });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME CLASS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
class DodgeGame {
|
||||
constructor(canvasId, config) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.config = config;
|
||||
|
||||
this.reset();
|
||||
this.running = false;
|
||||
this.lastTime = 0;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.playerX = this.canvas.width / 2;
|
||||
this.playerY = this.canvas.height - 80;
|
||||
this.score = 0;
|
||||
this.obstacles = [];
|
||||
this.coins = [];
|
||||
this.animTime = 0;
|
||||
this.spawnTimer = 0;
|
||||
this.coinTimer = 0;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.running = true;
|
||||
this.lastTime = performance.now();
|
||||
this.loop();
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
loop() {
|
||||
if (!this.running) return;
|
||||
|
||||
const now = performance.now();
|
||||
const dt = Math.min((now - this.lastTime) / 1000, 0.05);
|
||||
this.lastTime = now;
|
||||
|
||||
this.update(dt);
|
||||
this.render();
|
||||
|
||||
requestAnimationFrame(() => this.loop());
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
const speed = 200;
|
||||
|
||||
// Player movement
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) {
|
||||
this.playerX = Math.max(30, this.playerX - speed * dt);
|
||||
}
|
||||
if (keys['ArrowRight'] || keys['KeyD']) {
|
||||
this.playerX = Math.min(this.canvas.width - 30, this.playerX + speed * dt);
|
||||
}
|
||||
|
||||
// Spawn obstacles
|
||||
this.spawnTimer += dt;
|
||||
if (this.spawnTimer > 1.5) {
|
||||
this.spawnTimer = 0;
|
||||
this.obstacles.push({
|
||||
x: 50 + Math.random() * (this.canvas.width - 100),
|
||||
y: -30,
|
||||
size: 20 + Math.random() * 20
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn coins
|
||||
this.coinTimer += dt;
|
||||
if (this.coinTimer > 2) {
|
||||
this.coinTimer = 0;
|
||||
this.coins.push({
|
||||
x: 50 + Math.random() * (this.canvas.width - 100),
|
||||
y: -20
|
||||
});
|
||||
}
|
||||
|
||||
// Update obstacles
|
||||
this.obstacles = this.obstacles.filter(o => {
|
||||
o.y += 150 * dt;
|
||||
|
||||
// Collision
|
||||
const dx = this.playerX - o.x;
|
||||
const dy = this.playerY - o.y;
|
||||
if (Math.sqrt(dx*dx + dy*dy) < o.size + 25) {
|
||||
this.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
return o.y < this.canvas.height + 50;
|
||||
});
|
||||
|
||||
// Update coins
|
||||
this.coins = this.coins.filter(c => {
|
||||
c.y += 120 * dt;
|
||||
|
||||
// Collection
|
||||
const dx = this.playerX - c.x;
|
||||
const dy = this.playerY - c.y;
|
||||
if (Math.sqrt(dx*dx + dy*dy) < 35) {
|
||||
this.score += 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
return c.y < this.canvas.height + 30;
|
||||
});
|
||||
|
||||
this.score += Math.floor(dt * 5);
|
||||
this.animTime += dt * 1000;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ctx, canvas, config } = this;
|
||||
|
||||
// Background
|
||||
if (window.BackgroundEngine) {
|
||||
window.BackgroundEngine.render(
|
||||
ctx,
|
||||
config.assets.background.theme,
|
||||
config.assets.background.variant,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
this.animTime
|
||||
);
|
||||
} else {
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// Obstacles
|
||||
ctx.fillStyle = config.colors.obstacle;
|
||||
this.obstacles.forEach(o => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(o.x, o.y - o.size);
|
||||
ctx.lineTo(o.x + o.size, o.y + o.size);
|
||||
ctx.lineTo(o.x - o.size, o.y + o.size);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Coins
|
||||
ctx.fillStyle = config.colors.coin;
|
||||
this.coins.forEach(c => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(c.x, c.y, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Player
|
||||
ctx.save();
|
||||
ctx.translate(this.playerX, this.playerY);
|
||||
|
||||
const mode = config.assets.avatar.mode;
|
||||
const dims = {
|
||||
'HEAD_ONLY': { w: 64, h: 64, offsetY: 32 },
|
||||
'HEAD_AND_SHOULDERS': { w: 64, h: 96, offsetY: 48 },
|
||||
'UPPER_BODY': { w: 64, h: 128, offsetY: 64 },
|
||||
'FULL_BODY': { w: 64, h: 160, offsetY: 80 }
|
||||
};
|
||||
const d = dims[mode];
|
||||
|
||||
if (window.AvatarSystem) {
|
||||
ctx.translate(-32, -d.offsetY);
|
||||
window.AvatarSystem.render(
|
||||
ctx,
|
||||
playerAvatar,
|
||||
mode,
|
||||
'idle',
|
||||
this.animTime
|
||||
);
|
||||
} else {
|
||||
// Fallback
|
||||
ctx.fillStyle = config.colors.player;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 25, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Score
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 16px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`Score: ${this.score}`, 10, 25);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// CONTROLS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function startAll() {
|
||||
Object.values(games).forEach(g => g.start());
|
||||
}
|
||||
|
||||
function pauseAll() {
|
||||
Object.values(games).forEach(g => g.pause());
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
Object.values(games).forEach(g => {
|
||||
g.reset();
|
||||
g.render();
|
||||
});
|
||||
}
|
||||
|
||||
function showCode(theme) {
|
||||
document.querySelectorAll('.code-tab').forEach(t => t.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
const config = CONFIGS[theme];
|
||||
const code = `const GAME_CONFIG = {
|
||||
title: "${config.title}",
|
||||
assets: {
|
||||
music: { mood: "${config.assets.music.mood}", energy: ${config.assets.music.energy}, enabled: true },
|
||||
background: { theme: "${config.assets.background.theme}", variant: "${config.assets.background.variant}", animated: true },
|
||||
avatarContext: {
|
||||
mode: "${config.assets.avatar.mode}",
|
||||
context: "${config.assets.avatar.context}",
|
||||
showAccessories: true
|
||||
}
|
||||
}
|
||||
};`;
|
||||
document.getElementById('codeDisplay').textContent = code;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// Create game instances
|
||||
games.space = new DodgeGame('game1', CONFIGS.space);
|
||||
games.nature = new DodgeGame('game2', CONFIGS.nature);
|
||||
games.spooky = new DodgeGame('game3', CONFIGS.spooky);
|
||||
games.retro = new DodgeGame('game4', CONFIGS.retro);
|
||||
|
||||
// Initial render
|
||||
Object.values(games).forEach(g => g.render());
|
||||
|
||||
// Auto-start after a moment
|
||||
setTimeout(startAll, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,533 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,533 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,702 @@
|
||||
<!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>Night Racer - Racing Example</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; overflow: hidden; background: #0a0a15; touch-action: none; }
|
||||
canvas { display: block; }
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
text-shadow: 0 0 10px #0ff;
|
||||
z-index: 10;
|
||||
}
|
||||
#ui div { margin-bottom: 5px; }
|
||||
#speedometer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
color: #0ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 20px #0ff;
|
||||
}
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(135deg, #0a0a15, #1a1a2e);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #0ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
z-index: 100;
|
||||
}
|
||||
#loading h1 {
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 0 20px #0ff;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.loading-bar {
|
||||
width: 250px;
|
||||
height: 4px;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #0ff;
|
||||
}
|
||||
.loading-bar-fill {
|
||||
height: 100%;
|
||||
background: #0ff;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
box-shadow: 0 0 10px #0ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<h1>NIGHT RACER</h1>
|
||||
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
|
||||
<p id="loadingText">Initializing...</p>
|
||||
</div>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div id="ui">
|
||||
<div>SCORE: <span id="score">0</span></div>
|
||||
<div>DISTANCE: <span id="distance">0</span>m</div>
|
||||
</div>
|
||||
<div id="speedometer"><span id="speed">0</span> km/h</div>
|
||||
|
||||
<!-- Asset Libraries -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/urban.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>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RACER EXAMPLE - Vehicle Context Demo
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Demonstrates: HEAD_AND_SHOULDERS avatar, race-car context,
|
||||
// urban/neonDistrict background, Intense music mood
|
||||
|
||||
const GAME_CONFIG = {
|
||||
title: "Night Racer",
|
||||
assets: {
|
||||
music: { mood: "Intense", energy: 9, enabled: true },
|
||||
background: { theme: "urban", variant: "neonDistrict", animated: true },
|
||||
avatarContext: { mode: "HEAD_AND_SHOULDERS", context: "race-car", showAccessories: true }
|
||||
},
|
||||
gameplay: {
|
||||
baseSpeed: 300,
|
||||
maxSpeed: 600,
|
||||
acceleration: 150,
|
||||
deceleration: 200,
|
||||
laneWidth: 100,
|
||||
lanes: 3,
|
||||
obstacleSpawnRate: 1200,
|
||||
coinSpawnRate: 800
|
||||
}
|
||||
};
|
||||
|
||||
const playerAvatar = {
|
||||
faceShape: 'square',
|
||||
skinTone: 'tan',
|
||||
hairStyle: 'short',
|
||||
hairColor: '#1a1a1a',
|
||||
eyeShape: 'sharp',
|
||||
eyeColor: '#2ecc71',
|
||||
noseShape: 'pointed',
|
||||
mouthShape: 'serious'
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 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 = {
|
||||
playerLane: 1,
|
||||
playerX: 0,
|
||||
targetX: 0,
|
||||
speed: 0,
|
||||
distance: 0,
|
||||
animTime: 0,
|
||||
|
||||
score: 0,
|
||||
gameOver: false,
|
||||
paused: false,
|
||||
lastTime: 0,
|
||||
|
||||
obstacles: [],
|
||||
coins: [],
|
||||
roadLines: [],
|
||||
buildings: [],
|
||||
lastObstacle: 0,
|
||||
lastCoin: 0
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// LOADING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
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 city...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(40, 'Loading music...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(60, 'Loading vehicle...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(80, 'Starting engine...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(100, 'GO!');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
hideLoading();
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
|
||||
try {
|
||||
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOGIC
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initGame() {
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
|
||||
state.playerLane = 1;
|
||||
state.playerX = roadX + laneWidth * state.playerLane + laneWidth / 2;
|
||||
state.targetX = state.playerX;
|
||||
state.speed = GAME_CONFIG.gameplay.baseSpeed;
|
||||
state.distance = 0;
|
||||
state.score = 0;
|
||||
state.obstacles = [];
|
||||
state.coins = [];
|
||||
state.lastObstacle = 0;
|
||||
state.lastCoin = 0;
|
||||
|
||||
// Initialize road lines
|
||||
state.roadLines = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
state.roadLines.push({
|
||||
y: i * 80,
|
||||
lane: 0
|
||||
});
|
||||
state.roadLines.push({
|
||||
y: i * 80,
|
||||
lane: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize buildings
|
||||
state.buildings = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
state.buildings.push({
|
||||
x: Math.random() > 0.5 ? -50 - Math.random() * 100 : canvas.width + 50 + Math.random() * 100,
|
||||
y: i * 100 - 500,
|
||||
width: 60 + Math.random() * 100,
|
||||
height: 150 + Math.random() * 300,
|
||||
color: `hsl(${260 + Math.random() * 40}, 50%, ${15 + Math.random() * 15}%)`
|
||||
});
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function getLaneX(lane) {
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
return roadX + laneWidth * lane + laneWidth / 2;
|
||||
}
|
||||
|
||||
function updateGame(dt) {
|
||||
const { baseSpeed, maxSpeed, acceleration, deceleration, laneWidth, lanes, obstacleSpawnRate, coinSpawnRate } = GAME_CONFIG.gameplay;
|
||||
|
||||
// Speed control
|
||||
if (keys['ArrowUp'] || keys['KeyW']) {
|
||||
state.speed = Math.min(state.speed + acceleration * dt, maxSpeed);
|
||||
} else {
|
||||
state.speed = Math.max(state.speed - deceleration * dt * 0.3, baseSpeed);
|
||||
}
|
||||
|
||||
// Lane switching
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) {
|
||||
if (!keys._leftPressed) {
|
||||
keys._leftPressed = true;
|
||||
state.playerLane = Math.max(0, state.playerLane - 1);
|
||||
state.targetX = getLaneX(state.playerLane);
|
||||
}
|
||||
} else {
|
||||
keys._leftPressed = false;
|
||||
}
|
||||
|
||||
if (keys['ArrowRight'] || keys['KeyD']) {
|
||||
if (!keys._rightPressed) {
|
||||
keys._rightPressed = true;
|
||||
state.playerLane = Math.min(lanes - 1, state.playerLane + 1);
|
||||
state.targetX = getLaneX(state.playerLane);
|
||||
}
|
||||
} else {
|
||||
keys._rightPressed = false;
|
||||
}
|
||||
|
||||
// Smooth lane movement
|
||||
state.playerX += (state.targetX - state.playerX) * 10 * dt;
|
||||
|
||||
// Update distance
|
||||
state.distance += state.speed * dt * 0.01;
|
||||
|
||||
// Spawn obstacles
|
||||
state.lastObstacle += dt * 1000;
|
||||
if (state.lastObstacle > obstacleSpawnRate * (baseSpeed / state.speed)) {
|
||||
state.lastObstacle = 0;
|
||||
const lane = Math.floor(Math.random() * lanes);
|
||||
state.obstacles.push({
|
||||
x: getLaneX(lane),
|
||||
y: -100,
|
||||
lane: lane,
|
||||
type: Math.random() > 0.7 ? 'truck' : 'car',
|
||||
color: `hsl(${Math.random() * 360}, 70%, 50%)`
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn coins
|
||||
state.lastCoin += dt * 1000;
|
||||
if (state.lastCoin > coinSpawnRate) {
|
||||
state.lastCoin = 0;
|
||||
const lane = Math.floor(Math.random() * lanes);
|
||||
// Don't spawn on obstacles
|
||||
const blocked = state.obstacles.some(o => o.lane === lane && o.y < 200);
|
||||
if (!blocked) {
|
||||
state.coins.push({
|
||||
x: getLaneX(lane),
|
||||
y: -50,
|
||||
lane: lane,
|
||||
rotation: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update obstacles
|
||||
state.obstacles = state.obstacles.filter(obs => {
|
||||
obs.y += state.speed * dt;
|
||||
|
||||
// Collision check
|
||||
const playerY = canvas.height - 150;
|
||||
if (Math.abs(obs.x - state.playerX) < 40 &&
|
||||
Math.abs(obs.y - playerY) < 60) {
|
||||
endGame();
|
||||
}
|
||||
|
||||
return obs.y < canvas.height + 150;
|
||||
});
|
||||
|
||||
// Update coins
|
||||
state.coins = state.coins.filter(coin => {
|
||||
coin.y += state.speed * dt;
|
||||
coin.rotation += dt * 5;
|
||||
|
||||
// Collection check
|
||||
const playerY = canvas.height - 150;
|
||||
if (Math.abs(coin.x - state.playerX) < 50 &&
|
||||
Math.abs(coin.y - playerY) < 50) {
|
||||
state.score += 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
return coin.y < canvas.height + 50;
|
||||
});
|
||||
|
||||
// Update road lines
|
||||
state.roadLines.forEach(line => {
|
||||
line.y += state.speed * dt;
|
||||
if (line.y > canvas.height) {
|
||||
line.y -= 15 * 80;
|
||||
}
|
||||
});
|
||||
|
||||
// Update buildings
|
||||
state.buildings.forEach(b => {
|
||||
b.y += state.speed * dt * 0.3;
|
||||
if (b.y > canvas.height + 200) {
|
||||
b.y -= 20 * 100 + 500;
|
||||
b.height = 150 + Math.random() * 300;
|
||||
}
|
||||
});
|
||||
|
||||
// Score over time
|
||||
state.score += Math.floor(state.speed * dt * 0.05);
|
||||
|
||||
state.animTime += dt * 1000;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function renderGame() {
|
||||
// Dark background
|
||||
ctx.fillStyle = '#0a0a15';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// City skyline (buildings)
|
||||
state.buildings.forEach(b => {
|
||||
// Building
|
||||
ctx.fillStyle = b.color;
|
||||
const bx = b.x < 0 ? b.x : b.x;
|
||||
ctx.fillRect(bx, b.y, b.width, b.height);
|
||||
|
||||
// Windows
|
||||
ctx.fillStyle = Math.random() > 0.3 ? '#ffff88' : '#333';
|
||||
for (let wy = b.y + 20; wy < b.y + b.height - 20; wy += 30) {
|
||||
for (let wx = bx + 10; wx < bx + b.width - 10; wx += 20) {
|
||||
if (Math.random() > 0.4) {
|
||||
ctx.fillRect(wx, wy, 10, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes + 40;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
|
||||
// Road
|
||||
ctx.fillStyle = '#1a1a2a';
|
||||
ctx.fillRect(roadX, 0, roadWidth, canvas.height);
|
||||
|
||||
// Road edges (neon)
|
||||
ctx.strokeStyle = '#ff00ff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.shadowColor = '#ff00ff';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(roadX, 0);
|
||||
ctx.lineTo(roadX, canvas.height);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(roadX + roadWidth, 0);
|
||||
ctx.lineTo(roadX + roadWidth, canvas.height);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Lane dividers
|
||||
ctx.strokeStyle = '#0ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.shadowColor = '#0ff';
|
||||
ctx.shadowBlur = 10;
|
||||
state.roadLines.forEach(line => {
|
||||
const x1 = roadX + 20 + laneWidth * (line.lane + 1);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, line.y);
|
||||
ctx.lineTo(x1, line.y + 40);
|
||||
ctx.stroke();
|
||||
});
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Coins
|
||||
state.coins.forEach(coin => {
|
||||
ctx.save();
|
||||
ctx.translate(coin.x, coin.y);
|
||||
ctx.rotate(coin.rotation);
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.shadowColor = '#FFD700';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#FFA500';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-4, -4, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Obstacles (other cars)
|
||||
state.obstacles.forEach(obs => {
|
||||
ctx.save();
|
||||
ctx.translate(obs.x, obs.y);
|
||||
|
||||
if (obs.type === 'truck') {
|
||||
// Truck
|
||||
ctx.fillStyle = obs.color;
|
||||
ctx.fillRect(-25, -60, 50, 100);
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-20, -55, 40, 25);
|
||||
// Wheels
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(-28, -40, 8, 20);
|
||||
ctx.fillRect(20, -40, 8, 20);
|
||||
ctx.fillRect(-28, 20, 8, 20);
|
||||
ctx.fillRect(20, 20, 8, 20);
|
||||
} else {
|
||||
// Car
|
||||
ctx.fillStyle = obs.color;
|
||||
ctx.fillRect(-20, -40, 40, 70);
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-15, -35, 30, 20);
|
||||
// Tail lights
|
||||
ctx.fillStyle = '#ff0000';
|
||||
ctx.shadowColor = '#ff0000';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.fillRect(-18, 25, 8, 5);
|
||||
ctx.fillRect(10, 25, 8, 5);
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Player car with avatar
|
||||
const playerY = canvas.height - 150;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(state.playerX, playerY);
|
||||
|
||||
// Car body
|
||||
ctx.fillStyle = '#e74c3c';
|
||||
ctx.shadowColor = '#e74c3c';
|
||||
ctx.shadowBlur = 20;
|
||||
|
||||
// Main body
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-30, 50);
|
||||
ctx.lineTo(-35, 20);
|
||||
ctx.lineTo(-30, -30);
|
||||
ctx.lineTo(-20, -50);
|
||||
ctx.lineTo(20, -50);
|
||||
ctx.lineTo(30, -30);
|
||||
ctx.lineTo(35, 20);
|
||||
ctx.lineTo(30, 50);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Windshield
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, -45);
|
||||
ctx.lineTo(-25, -15);
|
||||
ctx.lineTo(25, -15);
|
||||
ctx.lineTo(20, -45);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Avatar in car (head and shoulders visible)
|
||||
if (window.ContextRenderer) {
|
||||
ctx.save();
|
||||
ctx.translate(-32, -65);
|
||||
window.ContextRenderer.render(
|
||||
ctx,
|
||||
GAME_CONFIG.assets.avatarContext.context,
|
||||
playerAvatar,
|
||||
32, 32,
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
ctx.restore();
|
||||
} else if (window.AvatarSystem) {
|
||||
ctx.save();
|
||||
ctx.translate(-32, -65);
|
||||
window.AvatarSystem.render(
|
||||
ctx,
|
||||
playerAvatar,
|
||||
'HEAD_AND_SHOULDERS',
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Headlights
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.shadowColor = '#fff';
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-18, -45, 5, 0, Math.PI * 2);
|
||||
ctx.arc(18, -45, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Light beams
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, -50);
|
||||
ctx.lineTo(-40, -200);
|
||||
ctx.lineTo(0, -200);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, -50);
|
||||
ctx.lineTo(0, -200);
|
||||
ctx.lineTo(40, -200);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Speed lines effect at high speed
|
||||
if (state.speed > 400) {
|
||||
ctx.strokeStyle = `rgba(0, 255, 255, ${(state.speed - 400) / 400})`;
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x, y + 50 + state.speed * 0.2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateUI() {
|
||||
document.getElementById('score').textContent = state.score;
|
||||
document.getElementById('distance').textContent = Math.floor(state.distance);
|
||||
document.getElementById('speed').textContent = Math.floor(state.speed * 0.6);
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.gameOver = true;
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (confirm(`GAME OVER\n\nScore: ${state.score}\nDistance: ${Math.floor(state.distance)}m\n\nRace again?`)) {
|
||||
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;
|
||||
keys['ArrowUp'] = true;
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
const x = e.touches[0].clientX;
|
||||
const dx = x - touchStartX;
|
||||
|
||||
if (dx < -50) {
|
||||
keys['ArrowLeft'] = true;
|
||||
keys['ArrowRight'] = false;
|
||||
touchStartX = x;
|
||||
} else if (dx > 50) {
|
||||
keys['ArrowRight'] = true;
|
||||
keys['ArrowLeft'] = false;
|
||||
touchStartX = x;
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
keys['ArrowUp'] = false;
|
||||
keys['ArrowLeft'] = false;
|
||||
keys['ArrowRight'] = 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);
|
||||
}
|
||||
|
||||
window.pauseGame = () => { state.paused = true; };
|
||||
window.resumeGame = () => {
|
||||
state.paused = false;
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
loadAssets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user