Initial commit

This commit is contained in:
Allen
2026-04-30 02:14:25 +00:00
commit 7090e6f01d
243 changed files with 115122 additions and 0 deletions
@@ -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>
+702
View File
@@ -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>