906 lines
32 KiB
HTML
906 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title><!-- GAME_TITLE --></title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #000;
|
|
touch-action: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
canvas { display: block; }
|
|
|
|
/* Game UI Overlay */
|
|
#ui {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
color: white;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
font-size: 16px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
|
z-index: 100;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Pause Menu */
|
|
#pauseMenu {
|
|
display: none;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0,0,0,0.9);
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
color: white;
|
|
font-family: Arial, sans-serif;
|
|
z-index: 200;
|
|
}
|
|
#pauseMenu h2 { margin-top: 0; }
|
|
#pauseMenu button {
|
|
display: block;
|
|
width: 200px;
|
|
margin: 10px auto;
|
|
padding: 12px;
|
|
font-size: 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
background: #4CAF50;
|
|
color: white;
|
|
}
|
|
#pauseMenu button:hover { background: #45a049; }
|
|
|
|
/* Loading Screen */
|
|
#loading {
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: #1a1a2e;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-family: Arial, sans-serif;
|
|
z-index: 300;
|
|
}
|
|
#loading .spinner {
|
|
width: 50px;
|
|
height: 50px;
|
|
border: 4px solid #333;
|
|
border-top-color: #4CAF50;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
#loading .progress {
|
|
margin-top: 20px;
|
|
font-size: 14px;
|
|
color: #888;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Loading Screen -->
|
|
<div id="loading">
|
|
<div class="spinner"></div>
|
|
<div class="progress">Loading assets...</div>
|
|
</div>
|
|
|
|
<!-- Game Canvas -->
|
|
<canvas id="gameCanvas"></canvas>
|
|
|
|
<!-- HUD Overlay -->
|
|
<div id="ui">
|
|
<div id="score">Score: 0</div>
|
|
<div id="health">Health: 100</div>
|
|
<!-- HAIKU: Add additional UI elements here -->
|
|
</div>
|
|
|
|
<!-- Pause Menu -->
|
|
<div id="pauseMenu">
|
|
<h2>PAUSED</h2>
|
|
<button onclick="resumeGame()">Resume</button>
|
|
<button onclick="toggleMusic()">Music: ON</button>
|
|
<button onclick="toggleSfx()">Sound: ON</button>
|
|
<button onclick="restartGame()">Restart</button>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════════════════════════════════════════════════
|
|
ASSET LIBRARY SCRIPTS
|
|
These provide: Music, Backgrounds, Avatars, Contexts
|
|
═══════════════════════════════════════════════════════════════════ -->
|
|
|
|
<!-- Music System (160 procedural songs) -->
|
|
<script src="/assets/music/moodCategories.js"></script>
|
|
<script src="/assets/music/musicLibrary.js"></script>
|
|
<script src="/assets/music/musicEngine.js"></script>
|
|
|
|
<!-- Background System (120 procedural backgrounds) -->
|
|
<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/urban.js"></script>
|
|
<script src="/assets/backgrounds/themes/fantasy.js"></script>
|
|
<script src="/assets/backgrounds/themes/abstract.js"></script>
|
|
<script src="/assets/backgrounds/themes/retro.js"></script>
|
|
<script src="/assets/backgrounds/themes/indoor.js"></script>
|
|
<script src="/assets/backgrounds/themes/weather.js"></script>
|
|
<script src="/assets/backgrounds/themes/sports.js"></script>
|
|
<script src="/assets/backgrounds/themes/seasonal.js"></script>
|
|
<script src="/assets/backgrounds/themes/underwater.js"></script>
|
|
<script src="/assets/backgrounds/themes/sky.js"></script>
|
|
<script src="/assets/backgrounds/themes/mechanical.js"></script>
|
|
<script src="/assets/backgrounds/themes/spooky.js"></script>
|
|
<script src="/assets/backgrounds/themes/colorful.js"></script>
|
|
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
|
|
|
<!-- Avatar System -->
|
|
<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>
|
|
|
|
<!-- Context System (26 game contexts) -->
|
|
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
|
<script src="/assets/avatar/contexts/costumes.js"></script>
|
|
<script src="/assets/avatar/contexts/pure.js"></script>
|
|
<script src="/assets/avatar/contextCatalog.js"></script>
|
|
<script src="/assets/avatar/contextRenderer.js"></script>
|
|
|
|
<!-- Asset Manager (Unified coordination) -->
|
|
<script src="/assets/assetCatalog.js"></script>
|
|
<script src="/assets/compatibility.js"></script>
|
|
<script src="/assets/presets.js"></script>
|
|
<script src="/assets/assetManager.js"></script>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME CONFIGURATION
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// HAIKU: Fill in the values below based on the game's theme and mechanics
|
|
|
|
const GAME_CONFIG = {
|
|
// Game metadata
|
|
title: "<!-- GAME_TITLE -->",
|
|
version: "1.0.0",
|
|
|
|
// Asset configuration
|
|
assets: {
|
|
// Music settings
|
|
// Moods: Epic, Chill, Intense, Playful, Mysterious, Heroic, Quirky, Ambient
|
|
music: {
|
|
mood: "<!-- MUSIC_MOOD -->", // e.g., "Epic", "Playful", "Mysterious"
|
|
energy: 7, // 1-10 (affects tempo/intensity)
|
|
enabled: true,
|
|
fadeInDuration: 1000 // ms to fade in music
|
|
},
|
|
|
|
// Background settings
|
|
// Themes: space, nature, urban, fantasy, abstract, retro, indoor, weather,
|
|
// sports, seasonal, underwater, sky, mechanical, spooky, colorful
|
|
background: {
|
|
theme: "<!-- BG_THEME -->", // e.g., "space", "nature", "fantasy"
|
|
variant: "<!-- BG_VARIANT -->", // e.g., "nebula", "forest", "castle"
|
|
animated: true, // Enable background animation
|
|
parallax: false, // Enable parallax scrolling
|
|
effects: [] // Additional effects: ["stars", "rain", etc]
|
|
},
|
|
|
|
// Avatar context settings
|
|
// Modes: HEAD_ONLY, HEAD_AND_SHOULDERS, UPPER_BODY, FULL_BODY
|
|
// Contexts: spaceship-cockpit, race-car, platformer-standard, knight-armor, etc.
|
|
avatarContext: {
|
|
mode: "<!-- AVATAR_MODE -->", // Display mode
|
|
context: "<!-- CONTEXT_TYPE -->", // Context ID
|
|
showAccessories: true, // Show earned accessories
|
|
scale: 1.0 // Avatar scale multiplier
|
|
},
|
|
|
|
// Color palette (optional override)
|
|
// Palettes: space-blue, forest-green, sunset-orange, ocean-teal, neon-pink,
|
|
// retro-arcade, fantasy-purple, spooky-grey, sports-red, nature-brown
|
|
colorPalette: null // null = auto-detect from theme
|
|
},
|
|
|
|
// Gameplay settings
|
|
// HAIKU: Define game-specific variables here
|
|
gameplay: {
|
|
// <!-- GAMEPLAY_CONFIG -->
|
|
// Example fields (customize per game):
|
|
// playerSpeed: 5,
|
|
// gravity: 0.5,
|
|
// jumpForce: 12,
|
|
// spawnRate: 2000,
|
|
// difficulty: 1
|
|
},
|
|
|
|
// Control scheme
|
|
controls: {
|
|
type: "<!-- CONTROL_TYPE -->", // "keyboard", "touch", "mouse", "tilt"
|
|
// HAIKU: Define specific bindings in input handlers below
|
|
}
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME STATE
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Core state
|
|
let gameState = {
|
|
// Player
|
|
player: null, // Avatar with context
|
|
playerX: 0,
|
|
playerY: 0,
|
|
playerVelX: 0,
|
|
playerVelY: 0,
|
|
|
|
// Game progress
|
|
score: 0,
|
|
health: 100,
|
|
level: 1,
|
|
|
|
// Game status
|
|
isRunning: false,
|
|
isPaused: false,
|
|
isGameOver: false,
|
|
|
|
// Timing
|
|
lastTime: 0,
|
|
deltaTime: 0,
|
|
elapsedTime: 0,
|
|
|
|
// Animation
|
|
currentAnimation: 'idle',
|
|
animationTime: 0,
|
|
|
|
// HAIKU: Add game-specific state variables below
|
|
// <!-- GAME_STATE -->
|
|
};
|
|
|
|
// Settings (persisted)
|
|
let settings = {
|
|
musicEnabled: true,
|
|
sfxEnabled: true,
|
|
musicVolume: 0.7,
|
|
sfxVolume: 0.8
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// INITIALIZATION
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
// Resize canvas to fill screen
|
|
function resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
// Update loading progress
|
|
function setLoadingProgress(message) {
|
|
const progress = document.querySelector('#loading .progress');
|
|
if (progress) progress.textContent = message;
|
|
}
|
|
|
|
// Main initialization
|
|
async function init() {
|
|
try {
|
|
setLoadingProgress('Initializing asset systems...');
|
|
|
|
// Initialize asset manager
|
|
await AssetManager.initialize();
|
|
|
|
setLoadingProgress('Loading music...');
|
|
|
|
// Setup music
|
|
if (GAME_CONFIG.assets.music.enabled && window.MusicEngine) {
|
|
MusicEngine.setVolume(settings.musicVolume);
|
|
}
|
|
|
|
setLoadingProgress('Loading background...');
|
|
|
|
// Background is rendered each frame, no preload needed
|
|
|
|
setLoadingProgress('Loading avatar...');
|
|
|
|
// Load user's avatar
|
|
const userAvatar = await loadUserAvatar();
|
|
|
|
// Apply game context to avatar
|
|
if (window.ContextRenderer) {
|
|
gameState.player = ContextRenderer.apply(
|
|
userAvatar,
|
|
GAME_CONFIG.assets.avatarContext.context
|
|
);
|
|
} else {
|
|
// Fallback: use avatar directly
|
|
gameState.player = { avatar: userAvatar, context: null };
|
|
}
|
|
|
|
// Position player
|
|
gameState.playerX = canvas.width / 2;
|
|
gameState.playerY = canvas.height / 2;
|
|
|
|
setLoadingProgress('Starting game...');
|
|
|
|
// Initialize game-specific logic
|
|
initGame();
|
|
|
|
// Hide loading screen
|
|
document.getElementById('loading').style.display = 'none';
|
|
|
|
// Start music
|
|
if (GAME_CONFIG.assets.music.enabled && window.MusicEngine) {
|
|
MusicEngine.playRandomSong(GAME_CONFIG.assets.music.mood);
|
|
}
|
|
|
|
// Start game loop
|
|
gameState.isRunning = true;
|
|
gameState.lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
|
|
} catch (error) {
|
|
console.error('Game initialization failed:', error);
|
|
setLoadingProgress('Error loading game. Please refresh.');
|
|
}
|
|
}
|
|
|
|
// Load user avatar from API or use default
|
|
async function loadUserAvatar() {
|
|
try {
|
|
// Try to get user profile from parent window (if in iframe)
|
|
if (window.parent && window.parent !== window) {
|
|
const message = await new Promise((resolve, reject) => {
|
|
const handler = (e) => {
|
|
if (e.data && e.data.type === 'avatarData') {
|
|
window.removeEventListener('message', handler);
|
|
resolve(e.data.avatar);
|
|
}
|
|
};
|
|
window.addEventListener('message', handler);
|
|
window.parent.postMessage({ type: 'requestAvatar' }, '*');
|
|
setTimeout(() => reject(new Error('Timeout')), 2000);
|
|
});
|
|
|
|
if (message && window.AvatarSystem) {
|
|
return AvatarSystem.importMinimal(message, message.userId || 'player');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Fall through to default
|
|
}
|
|
|
|
// Return default avatar
|
|
if (window.AvatarSystem) {
|
|
return AvatarSystem.createDefault();
|
|
}
|
|
|
|
// Minimal fallback
|
|
return { base: { skinTone: '#FFD5B8' } };
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// UTILITY FUNCTIONS
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
// Update score
|
|
function updateScore(points) {
|
|
gameState.score += points;
|
|
document.getElementById('score').textContent = `Score: ${gameState.score}`;
|
|
|
|
// Notify parent window
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage({ type: 'scoreUpdate', score: gameState.score }, '*');
|
|
}
|
|
}
|
|
|
|
// Update health
|
|
function updateHealth(amount) {
|
|
gameState.health = Math.max(0, Math.min(100, gameState.health + amount));
|
|
document.getElementById('health').textContent = `Health: ${gameState.health}`;
|
|
|
|
// Trigger hurt animation
|
|
if (amount < 0) {
|
|
setPlayerAnimation('hurt');
|
|
setTimeout(() => setPlayerAnimation('idle'), 500);
|
|
}
|
|
|
|
// Check for game over
|
|
if (gameState.health <= 0) {
|
|
endGame();
|
|
}
|
|
}
|
|
|
|
// Set player animation
|
|
function setPlayerAnimation(animationId) {
|
|
if (gameState.currentAnimation !== animationId) {
|
|
gameState.currentAnimation = animationId;
|
|
gameState.animationTime = 0;
|
|
|
|
if (window.ContextRenderer && gameState.player) {
|
|
ContextRenderer.animate(gameState.player, animationId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Play sound effect
|
|
function playSfx(name) {
|
|
if (!settings.sfxEnabled) return;
|
|
// HAIKU: Implement sound effects if needed
|
|
// Example: new Audio(`/sounds/${name}.mp3`).play();
|
|
}
|
|
|
|
// Random number helpers
|
|
function randomInt(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
function randomFloat(min, max) {
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
// Collision detection
|
|
function rectCollision(r1, r2) {
|
|
return r1.x < r2.x + r2.width &&
|
|
r1.x + r1.width > r2.x &&
|
|
r1.y < r2.y + r2.height &&
|
|
r1.y + r1.height > r2.y;
|
|
}
|
|
|
|
function circleCollision(c1, c2) {
|
|
const dx = c1.x - c2.x;
|
|
const dy = c1.y - c2.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
return dist < c1.radius + c2.radius;
|
|
}
|
|
|
|
// Clamp value
|
|
function clamp(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
// Lerp (linear interpolation)
|
|
function lerp(a, b, t) {
|
|
return a + (b - a) * t;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME LIFECYCLE
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function pauseGame() {
|
|
if (gameState.isGameOver) return;
|
|
gameState.isPaused = true;
|
|
document.getElementById('pauseMenu').style.display = 'block';
|
|
|
|
if (window.MusicEngine) {
|
|
MusicEngine.setVolume(settings.musicVolume * 0.3);
|
|
}
|
|
|
|
// Notify parent
|
|
if (window.pauseGame) window.pauseGame();
|
|
}
|
|
|
|
function resumeGame() {
|
|
gameState.isPaused = false;
|
|
document.getElementById('pauseMenu').style.display = 'none';
|
|
gameState.lastTime = performance.now();
|
|
|
|
if (window.MusicEngine) {
|
|
MusicEngine.setVolume(settings.musicVolume);
|
|
}
|
|
|
|
// Notify parent
|
|
if (window.resumeGame) window.resumeGame();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function toggleMusic() {
|
|
settings.musicEnabled = !settings.musicEnabled;
|
|
const btn = document.querySelector('#pauseMenu button:nth-child(2)');
|
|
btn.textContent = `Music: ${settings.musicEnabled ? 'ON' : 'OFF'}`;
|
|
|
|
if (window.MusicEngine) {
|
|
if (settings.musicEnabled) {
|
|
MusicEngine.setVolume(settings.musicVolume);
|
|
} else {
|
|
MusicEngine.setVolume(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleSfx() {
|
|
settings.sfxEnabled = !settings.sfxEnabled;
|
|
const btn = document.querySelector('#pauseMenu button:nth-child(3)');
|
|
btn.textContent = `Sound: ${settings.sfxEnabled ? 'ON' : 'OFF'}`;
|
|
}
|
|
|
|
function restartGame() {
|
|
// Reset state
|
|
gameState.score = 0;
|
|
gameState.health = 100;
|
|
gameState.level = 1;
|
|
gameState.isGameOver = false;
|
|
gameState.playerX = canvas.width / 2;
|
|
gameState.playerY = canvas.height / 2;
|
|
gameState.playerVelX = 0;
|
|
gameState.playerVelY = 0;
|
|
|
|
// Update UI
|
|
document.getElementById('score').textContent = 'Score: 0';
|
|
document.getElementById('health').textContent = 'Health: 100';
|
|
|
|
// Resume
|
|
resumeGame();
|
|
|
|
// Reinitialize game-specific state
|
|
initGame();
|
|
}
|
|
|
|
function endGame() {
|
|
gameState.isGameOver = true;
|
|
gameState.isRunning = false;
|
|
|
|
// Fade out music
|
|
if (window.MusicEngine) {
|
|
MusicEngine.fadeOut(2000);
|
|
}
|
|
|
|
// Report final score to parent
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage({
|
|
type: 'gameOver',
|
|
score: gameState.score,
|
|
screenshot: captureScreenshot()
|
|
}, '*');
|
|
}
|
|
|
|
// Show game over (parent handles this, but fallback)
|
|
setTimeout(() => {
|
|
if (confirm(`Game Over!\n\nFinal Score: ${gameState.score}\n\nPlay again?`)) {
|
|
restartGame();
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
function captureScreenshot() {
|
|
return canvas.toDataURL('image/png');
|
|
}
|
|
|
|
// Expose functions for parent window control
|
|
window.pauseGame = pauseGame;
|
|
window.resumeGame = resumeGame;
|
|
window.pauseMusic = () => { if (window.MusicEngine) MusicEngine.setVolume(0); };
|
|
window.resumeMusic = () => { if (window.MusicEngine) MusicEngine.setVolume(settings.musicVolume); };
|
|
window.setMusicEnabled = (enabled) => { settings.musicEnabled = enabled; toggleMusic(); toggleMusic(); };
|
|
window.setSfxEnabled = (enabled) => { settings.sfxEnabled = enabled; };
|
|
window.gameScore = () => gameState.score;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// RENDERING
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function renderBackground(time) {
|
|
const bg = GAME_CONFIG.assets.background;
|
|
|
|
if (window.BackgroundEngine) {
|
|
BackgroundEngine.render(
|
|
ctx,
|
|
bg.theme,
|
|
bg.variant,
|
|
canvas.width,
|
|
canvas.height,
|
|
time / 1000,
|
|
{ effects: bg.effects }
|
|
);
|
|
} else {
|
|
// Fallback: solid color
|
|
ctx.fillStyle = '#1a1a2e';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
}
|
|
|
|
function renderPlayer() {
|
|
const config = GAME_CONFIG.assets.avatarContext;
|
|
const scale = config.scale || 1;
|
|
|
|
if (window.ContextRenderer && gameState.player) {
|
|
ContextRenderer.render(
|
|
gameState.player,
|
|
ctx,
|
|
gameState.playerX,
|
|
gameState.playerY,
|
|
{
|
|
scale: scale,
|
|
time: gameState.elapsedTime / 1000,
|
|
animation: gameState.currentAnimation,
|
|
frame: Math.floor(gameState.animationTime / 100) // ~10 fps animation
|
|
}
|
|
);
|
|
} else if (window.AvatarRenderer && gameState.player) {
|
|
// Fallback: render avatar directly
|
|
AvatarRenderer.render(
|
|
ctx,
|
|
gameState.player.avatar || gameState.player,
|
|
config.mode,
|
|
gameState.playerX,
|
|
gameState.playerY,
|
|
{ scale: scale }
|
|
);
|
|
} else {
|
|
// Minimal fallback: draw circle
|
|
ctx.fillStyle = '#4CAF50';
|
|
ctx.beginPath();
|
|
ctx.arc(gameState.playerX, gameState.playerY, 30 * scale, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME LOOP
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
function gameLoop(currentTime) {
|
|
if (!gameState.isRunning || gameState.isPaused) return;
|
|
|
|
// Calculate delta time
|
|
gameState.deltaTime = currentTime - gameState.lastTime;
|
|
gameState.lastTime = currentTime;
|
|
gameState.elapsedTime += gameState.deltaTime;
|
|
gameState.animationTime += gameState.deltaTime;
|
|
|
|
// Clear canvas
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// 1. Render background
|
|
renderBackground(gameState.elapsedTime);
|
|
|
|
// 2. Update game logic
|
|
// HAIKU: Call your update function here
|
|
updateGame(gameState.deltaTime / 1000); // Pass delta in seconds
|
|
|
|
// 3. Render game objects
|
|
// HAIKU: Call your render function here
|
|
renderGame();
|
|
|
|
// 4. Render player
|
|
renderPlayer();
|
|
|
|
// 5. Render UI overlay (if any canvas-based UI)
|
|
// HAIKU: Render any canvas-based UI here
|
|
renderUI();
|
|
|
|
// Continue loop
|
|
if (!gameState.isGameOver) {
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// INPUT HANDLING
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
// Track pressed keys
|
|
const keys = {};
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
keys[e.code] = true;
|
|
|
|
// Pause on Escape
|
|
if (e.code === 'Escape') {
|
|
if (gameState.isPaused) resumeGame();
|
|
else pauseGame();
|
|
return;
|
|
}
|
|
|
|
// HAIKU: Handle game-specific key presses
|
|
handleKeyDown(e.code);
|
|
});
|
|
|
|
document.addEventListener('keyup', (e) => {
|
|
keys[e.code] = false;
|
|
|
|
// HAIKU: Handle game-specific key releases
|
|
handleKeyUp(e.code);
|
|
});
|
|
|
|
// Touch/mouse input
|
|
let touchStartX = 0, touchStartY = 0;
|
|
let isTouching = false;
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
touchStartX = e.clientX;
|
|
touchStartY = e.clientY;
|
|
isTouching = true;
|
|
handleTouchStart(e.clientX, e.clientY);
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
if (isTouching) {
|
|
handleTouchMove(e.clientX, e.clientY, e.clientX - touchStartX, e.clientY - touchStartY);
|
|
}
|
|
handleMouseMove(e.clientX, e.clientY);
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', (e) => {
|
|
isTouching = false;
|
|
handleTouchEnd(e.clientX, e.clientY);
|
|
});
|
|
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
touchStartX = touch.clientX;
|
|
touchStartY = touch.clientY;
|
|
isTouching = true;
|
|
handleTouchStart(touch.clientX, touch.clientY);
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchmove', (e) => {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
handleTouchMove(touch.clientX, touch.clientY, touch.clientX - touchStartX, touch.clientY - touchStartY);
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('touchend', (e) => {
|
|
isTouching = false;
|
|
handleTouchEnd(touchStartX, touchStartY);
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// GAME-SPECIFIC CODE
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// HAIKU: Implement the functions below with your game logic
|
|
|
|
/**
|
|
* Initialize game-specific state
|
|
* Called once at start and on restart
|
|
*/
|
|
function initGame() {
|
|
// HAIKU: Initialize your game objects, enemies, items, etc.
|
|
// Example:
|
|
// gameState.enemies = [];
|
|
// gameState.items = [];
|
|
// spawnInitialObjects();
|
|
|
|
setPlayerAnimation('idle');
|
|
}
|
|
|
|
/**
|
|
* Update game logic
|
|
* @param {number} dt - Delta time in seconds
|
|
*/
|
|
function updateGame(dt) {
|
|
// HAIKU: Update your game objects here
|
|
// Example:
|
|
// updateEnemies(dt);
|
|
// updateItems(dt);
|
|
// checkCollisions();
|
|
// spawnNewObjects();
|
|
|
|
// Example: Continuous key input for movement
|
|
// if (keys['ArrowLeft'] || keys['KeyA']) gameState.playerX -= 300 * dt;
|
|
// if (keys['ArrowRight'] || keys['KeyD']) gameState.playerX += 300 * dt;
|
|
// if (keys['ArrowUp'] || keys['KeyW']) gameState.playerY -= 300 * dt;
|
|
// if (keys['ArrowDown'] || keys['KeyS']) gameState.playerY += 300 * dt;
|
|
|
|
// Keep player in bounds
|
|
// gameState.playerX = clamp(gameState.playerX, 50, canvas.width - 50);
|
|
// gameState.playerY = clamp(gameState.playerY, 50, canvas.height - 50);
|
|
}
|
|
|
|
/**
|
|
* Render game objects (not player, not background)
|
|
*/
|
|
function renderGame() {
|
|
// HAIKU: Render your game objects here
|
|
// Example:
|
|
// gameState.enemies.forEach(enemy => renderEnemy(enemy));
|
|
// gameState.items.forEach(item => renderItem(item));
|
|
}
|
|
|
|
/**
|
|
* Render any canvas-based UI (health bars, indicators, etc.)
|
|
*/
|
|
function renderUI() {
|
|
// HAIKU: Render canvas-based UI elements
|
|
// Example health bar:
|
|
// const barWidth = 200;
|
|
// const healthPct = gameState.health / 100;
|
|
// ctx.fillStyle = '#333';
|
|
// ctx.fillRect(canvas.width - barWidth - 20, 20, barWidth, 20);
|
|
// ctx.fillStyle = healthPct > 0.3 ? '#4CAF50' : '#f44336';
|
|
// ctx.fillRect(canvas.width - barWidth - 20, 20, barWidth * healthPct, 20);
|
|
}
|
|
|
|
/**
|
|
* Handle key down event
|
|
* @param {string} code - Key code (e.g., 'Space', 'ArrowUp')
|
|
*/
|
|
function handleKeyDown(code) {
|
|
// HAIKU: Handle instant key actions (jump, shoot, etc.)
|
|
// Example:
|
|
// if (code === 'Space') jump();
|
|
// if (code === 'KeyX') shoot();
|
|
}
|
|
|
|
/**
|
|
* Handle key up event
|
|
* @param {string} code - Key code
|
|
*/
|
|
function handleKeyUp(code) {
|
|
// HAIKU: Handle key release actions
|
|
}
|
|
|
|
/**
|
|
* Handle touch/click start
|
|
* @param {number} x - X position
|
|
* @param {number} y - Y position
|
|
*/
|
|
function handleTouchStart(x, y) {
|
|
// HAIKU: Handle touch/click start
|
|
// Example: Check if clicked on a button, start drag, etc.
|
|
}
|
|
|
|
/**
|
|
* Handle touch/mouse move
|
|
* @param {number} x - Current X
|
|
* @param {number} y - Current Y
|
|
* @param {number} dx - Delta X from start
|
|
* @param {number} dy - Delta Y from start
|
|
*/
|
|
function handleTouchMove(x, y, dx, dy) {
|
|
// HAIKU: Handle drag/swipe
|
|
// Example: gameState.playerX = x;
|
|
}
|
|
|
|
/**
|
|
* Handle touch/click end
|
|
* @param {number} x - X position
|
|
* @param {number} y - Y position
|
|
*/
|
|
function handleTouchEnd(x, y) {
|
|
// HAIKU: Handle touch/click end
|
|
}
|
|
|
|
/**
|
|
* Handle mouse move (hover, not dragging)
|
|
* @param {number} x - X position
|
|
* @param {number} y - Y position
|
|
*/
|
|
function handleMouseMove(x, y) {
|
|
// HAIKU: Handle mouse hover
|
|
// Example: Aim at cursor
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// START GAME
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
// Wait for DOM then initialize
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|