Initial commit
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
# Game Template Guide for Haiku
|
||||
|
||||
This guide explains how to use the game templates to generate HTML5 canvas games for GamerComp.
|
||||
|
||||
## Template Selection
|
||||
|
||||
### `gameTemplateAssets.html` - Full Asset Library Template
|
||||
Use this template when the game needs:
|
||||
- Procedural background themes (120 options)
|
||||
- Procedural music (160 songs, 8 moods)
|
||||
- User's avatar with game contexts (26 contexts)
|
||||
- Accessories and effects
|
||||
|
||||
**Best for:** RPGs, adventure games, racing games, space shooters, platformers, any game where the player's avatar matters.
|
||||
|
||||
### `gameTemplateSimple.html` - Minimal Template
|
||||
Use this template when the game needs:
|
||||
- Simple graphics (shapes, basic sprites)
|
||||
- No avatar/user identity
|
||||
- Fast loading (no asset libraries)
|
||||
|
||||
**Best for:** Puzzle games, simple arcade games, physics toys, creative tools, games where the player controls an object (not an avatar).
|
||||
|
||||
---
|
||||
|
||||
## Using gameTemplateAssets.html
|
||||
|
||||
### Step 1: Fill in GAME_CONFIG
|
||||
|
||||
Replace the placeholder comments with actual values:
|
||||
|
||||
```javascript
|
||||
const GAME_CONFIG = {
|
||||
title: "Space Defender", // Replace <!-- GAME_TITLE -->
|
||||
|
||||
assets: {
|
||||
music: {
|
||||
mood: "Epic", // Replace <!-- MUSIC_MOOD -->
|
||||
energy: 8,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
background: {
|
||||
theme: "space", // Replace <!-- BG_THEME -->
|
||||
variant: "nebula", // Replace <!-- BG_VARIANT -->
|
||||
animated: true,
|
||||
effects: ["stars"]
|
||||
},
|
||||
|
||||
avatarContext: {
|
||||
mode: "HEAD_ONLY", // Replace <!-- AVATAR_MODE -->
|
||||
context: "spaceship-cockpit", // Replace <!-- CONTEXT_TYPE -->
|
||||
showAccessories: true
|
||||
}
|
||||
},
|
||||
|
||||
gameplay: {
|
||||
// Add your game-specific config
|
||||
enemySpeed: 200,
|
||||
spawnRate: 2000,
|
||||
bulletSpeed: 500
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Choose Asset Combinations
|
||||
|
||||
#### Music Moods (8 options)
|
||||
| Mood | Best For | Energy Range |
|
||||
|------|----------|--------------|
|
||||
| Epic | Boss battles, action games | 7-10 |
|
||||
| Chill | Puzzle games, casual games | 2-4 |
|
||||
| Intense | Racing, shooters, action | 8-10 |
|
||||
| Playful | Kids games, casual games | 5-7 |
|
||||
| Mysterious | Horror, mystery, exploration | 3-6 |
|
||||
| Heroic | Adventure, platformers | 6-8 |
|
||||
| Quirky | Puzzle, comedy, unique games | 4-7 |
|
||||
| Ambient | Background, meditation, creative | 1-3 |
|
||||
|
||||
#### Background Themes (15 themes × 8 variants each)
|
||||
| Theme | Variants | Best For |
|
||||
|-------|----------|----------|
|
||||
| space | nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole | Space games, sci-fi |
|
||||
| nature | forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow | Adventure, relaxing |
|
||||
| urban | citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley | Racing, action |
|
||||
| fantasy | castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower | RPG, adventure |
|
||||
| abstract | geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix | Puzzle, creative |
|
||||
| retro | pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave | Arcade, retro |
|
||||
| indoor | laboratory, classroom, bedroom, kitchen, arcade, office, library, gym | Simulation, casual |
|
||||
| weather | rain, snow, storm, sunset, sunrise, fog, lightning, aurora | Atmospheric |
|
||||
| sports | stadium, raceTrack, court, field, arena, pool, gym, skatepark | Sports games |
|
||||
| seasonal | springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical | Seasonal themes |
|
||||
| underwater | coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure | Ocean games |
|
||||
| sky | clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield | Flying games |
|
||||
| mechanical | gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab | Tech, puzzle |
|
||||
| spooky | hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows | Horror, Halloween |
|
||||
| colorful | rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism | Kids, casual |
|
||||
|
||||
#### Avatar Modes (4 options)
|
||||
| Mode | Dimensions | Use When |
|
||||
|------|------------|----------|
|
||||
| HEAD_ONLY | 64×64 | Cockpit view, flying games, maze games |
|
||||
| HEAD_AND_SHOULDERS | 64×96 | Driving games, submarine, conversation |
|
||||
| UPPER_BODY | 64×128 | Racing, shooting, hoverboard |
|
||||
| FULL_BODY | 64×160 | Platformers, RPGs, sports, adventure |
|
||||
|
||||
#### Avatar Contexts (26 options)
|
||||
|
||||
**Vehicle Contexts:**
|
||||
- `spaceship-cockpit` (HEAD_ONLY) - Space shooters
|
||||
- `race-car` (HEAD_AND_SHOULDERS) - Racing games
|
||||
- `airplane` (HEAD_AND_SHOULDERS) - Flying games
|
||||
- `flappy-style` (HEAD_ONLY) - Flappy bird clones
|
||||
- `tank` (HEAD_ONLY) - Tank games
|
||||
- `pacman-style` (HEAD_ONLY) - Maze games
|
||||
- `hoverboard` (UPPER_BODY) - Hoverboard games
|
||||
- `jetpack` (UPPER_BODY) - Jetpack games
|
||||
- `mech-suit` (HEAD_ONLY) - Mech combat
|
||||
- `submarine` (HEAD_AND_SHOULDERS) - Underwater
|
||||
- `dragon-rider` (FULL_BODY) - Dragon games
|
||||
- `motorcycle` (FULL_BODY) - Motorcycle racing
|
||||
|
||||
**Costume Contexts:**
|
||||
- `knight-armor` (FULL_BODY) - Medieval games
|
||||
- `space-suit` (FULL_BODY) - Space exploration
|
||||
- `ninja-outfit` (FULL_BODY) - Ninja games
|
||||
- `wizard-robes` (FULL_BODY) - Magic games
|
||||
- `superhero-cape` (FULL_BODY) - Hero games
|
||||
- `athlete-uniform` (FULL_BODY) - Sports
|
||||
- `pirate-outfit` (FULL_BODY) - Pirate games
|
||||
- `scientist-labcoat` (FULL_BODY) - Science games
|
||||
- `chef-outfit` (FULL_BODY) - Cooking games
|
||||
- `cowboy-gear` (FULL_BODY) - Western games
|
||||
|
||||
**Pure Contexts:**
|
||||
- `platformer-standard` (FULL_BODY) - Standard platformer
|
||||
- `sports-player` (FULL_BODY) - Sports games
|
||||
- `adventure-hero` (FULL_BODY) - Adventure games
|
||||
- `runner` (FULL_BODY) - Endless runners
|
||||
|
||||
### Step 3: Implement Game Functions
|
||||
|
||||
The template provides these functions for you to implement:
|
||||
|
||||
```javascript
|
||||
// Called once at start and on restart
|
||||
function initGame() {
|
||||
// Initialize enemies, items, level data
|
||||
state.enemies = [];
|
||||
state.items = [];
|
||||
}
|
||||
|
||||
// Called every frame (dt = seconds since last frame)
|
||||
function updateGame(dt) {
|
||||
// Update positions, check collisions, spawn objects
|
||||
state.enemies.forEach(e => {
|
||||
e.x += e.velX * dt;
|
||||
e.y += e.velY * dt;
|
||||
});
|
||||
|
||||
// Check player collision with enemies
|
||||
state.enemies.forEach(e => {
|
||||
if (circleCollision(
|
||||
{ x: state.playerX, y: state.playerY, r: 30 },
|
||||
{ x: e.x, y: e.y, r: e.radius }
|
||||
)) {
|
||||
updateHealth(-10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called every frame after updateGame
|
||||
function renderGame() {
|
||||
// Draw enemies, items, effects
|
||||
state.enemies.forEach(e => {
|
||||
ctx.fillStyle = e.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// Input handlers
|
||||
function handleKeyDown(code) {
|
||||
if (code === 'Space') {
|
||||
shoot(); // Your function
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart(x, y) {
|
||||
// Handle tap
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Use Provided Utilities
|
||||
|
||||
The template includes these utility functions:
|
||||
|
||||
```javascript
|
||||
// Score & Health
|
||||
updateScore(10); // Add 10 points
|
||||
updateHealth(-5); // Lose 5 health
|
||||
updateHealth(10); // Gain 10 health (capped at 100)
|
||||
|
||||
// Animation
|
||||
setPlayerAnimation('walk'); // idle, walk, run, jump, hurt, celebrate, etc.
|
||||
|
||||
// Math
|
||||
randomInt(1, 10); // Random integer 1-10
|
||||
randomFloat(0, 1); // Random float 0-1
|
||||
clamp(value, 0, 100); // Clamp to range
|
||||
lerp(0, 100, 0.5); // Linear interpolation = 50
|
||||
|
||||
// Collision
|
||||
rectCollision(r1, r2); // {x, y, width, height} objects
|
||||
circleCollision(c1, c2); // {x, y, radius} objects
|
||||
|
||||
// Game flow
|
||||
endGame(); // Trigger game over
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using gameTemplateSimple.html
|
||||
|
||||
For simple games without avatars:
|
||||
|
||||
```javascript
|
||||
const GAME_CONFIG = {
|
||||
title: "Catch the Stars",
|
||||
|
||||
background: {
|
||||
type: "gradient",
|
||||
colors: ["#0f0c29", "#302b63", "#24243e"]
|
||||
},
|
||||
|
||||
player: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
color: "#FFD700",
|
||||
speed: 400
|
||||
},
|
||||
|
||||
gameplay: {
|
||||
starSpawnRate: 1000,
|
||||
starFallSpeed: 200
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Then implement:
|
||||
- `initGame()` - Setup
|
||||
- `update(dt)` - Game logic
|
||||
- `render()` - Draw objects
|
||||
- Input handlers
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Platformer Movement
|
||||
```javascript
|
||||
const GRAVITY = 1500;
|
||||
const JUMP_FORCE = 600;
|
||||
|
||||
function update(dt) {
|
||||
// Horizontal movement
|
||||
if (keys['ArrowLeft']) state.playerVelX = -300;
|
||||
else if (keys['ArrowRight']) state.playerVelX = 300;
|
||||
else state.playerVelX *= 0.8; // Friction
|
||||
|
||||
// Apply gravity
|
||||
state.playerVelY += GRAVITY * dt;
|
||||
|
||||
// Move player
|
||||
state.playerX += state.playerVelX * dt;
|
||||
state.playerY += state.playerVelY * dt;
|
||||
|
||||
// Ground collision
|
||||
if (state.playerY > canvas.height - 50) {
|
||||
state.playerY = canvas.height - 50;
|
||||
state.playerVelY = 0;
|
||||
state.isGrounded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(code) {
|
||||
if (code === 'Space' && state.isGrounded) {
|
||||
state.playerVelY = -JUMP_FORCE;
|
||||
state.isGrounded = false;
|
||||
setPlayerAnimation('jump');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Space Shooter
|
||||
```javascript
|
||||
state.bullets = [];
|
||||
state.enemies = [];
|
||||
|
||||
function update(dt) {
|
||||
// Update bullets
|
||||
state.bullets = state.bullets.filter(b => {
|
||||
b.y -= 500 * dt;
|
||||
return b.y > 0;
|
||||
});
|
||||
|
||||
// Spawn enemies
|
||||
if (Math.random() < 0.02) {
|
||||
state.enemies.push({
|
||||
x: randomInt(50, canvas.width - 50),
|
||||
y: -30,
|
||||
radius: 20,
|
||||
color: '#ff4444'
|
||||
});
|
||||
}
|
||||
|
||||
// Update enemies
|
||||
state.enemies.forEach(e => e.y += 150 * dt);
|
||||
state.enemies = state.enemies.filter(e => e.y < canvas.height + 50);
|
||||
|
||||
// Collision: bullets vs enemies
|
||||
state.bullets.forEach(b => {
|
||||
state.enemies = state.enemies.filter(e => {
|
||||
if (circleCollision(b, e)) {
|
||||
updateScore(10);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyDown(code) {
|
||||
if (code === 'Space') {
|
||||
state.bullets.push({
|
||||
x: state.playerX,
|
||||
y: state.playerY - 30,
|
||||
radius: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Endless Runner
|
||||
```javascript
|
||||
let scrollSpeed = 300;
|
||||
let obstacles = [];
|
||||
let lastObstacle = 0;
|
||||
|
||||
function update(dt) {
|
||||
scrollSpeed += dt * 2; // Speed up over time
|
||||
|
||||
// Jump
|
||||
if (keys['Space'] && state.isGrounded) {
|
||||
state.playerVelY = -500;
|
||||
state.isGrounded = false;
|
||||
}
|
||||
|
||||
// Gravity
|
||||
state.playerVelY += 1200 * dt;
|
||||
state.playerY += state.playerVelY * dt;
|
||||
|
||||
// Ground
|
||||
const ground = canvas.height - 100;
|
||||
if (state.playerY > ground) {
|
||||
state.playerY = ground;
|
||||
state.isGrounded = true;
|
||||
}
|
||||
|
||||
// Spawn obstacles
|
||||
lastObstacle += dt * 1000;
|
||||
if (lastObstacle > 1500) {
|
||||
obstacles.push({ x: canvas.width, y: ground, w: 30, h: 50 });
|
||||
lastObstacle = 0;
|
||||
}
|
||||
|
||||
// Move obstacles
|
||||
obstacles = obstacles.filter(o => {
|
||||
o.x -= scrollSpeed * dt;
|
||||
return o.x > -50;
|
||||
});
|
||||
|
||||
// Score
|
||||
updateScore(Math.floor(dt * 10));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Combinations by Game Type
|
||||
|
||||
### Space Shooter
|
||||
```javascript
|
||||
music: { mood: "Epic", energy: 8 }
|
||||
background: { theme: "space", variant: "nebula" }
|
||||
avatarContext: { mode: "HEAD_ONLY", context: "spaceship-cockpit" }
|
||||
```
|
||||
|
||||
### Platformer
|
||||
```javascript
|
||||
music: { mood: "Playful", energy: 6 }
|
||||
background: { theme: "nature", variant: "forest" }
|
||||
avatarContext: { mode: "FULL_BODY", context: "platformer-standard" }
|
||||
```
|
||||
|
||||
### Racing
|
||||
```javascript
|
||||
music: { mood: "Intense", energy: 9 }
|
||||
background: { theme: "urban", variant: "street" }
|
||||
avatarContext: { mode: "HEAD_AND_SHOULDERS", context: "race-car" }
|
||||
```
|
||||
|
||||
### RPG/Adventure
|
||||
```javascript
|
||||
music: { mood: "Heroic", energy: 6 }
|
||||
background: { theme: "fantasy", variant: "castle" }
|
||||
avatarContext: { mode: "FULL_BODY", context: "knight-armor" }
|
||||
```
|
||||
|
||||
### Puzzle
|
||||
```javascript
|
||||
music: { mood: "Chill", energy: 3 }
|
||||
background: { theme: "abstract", variant: "minimalist" }
|
||||
// Use gameTemplateSimple.html instead
|
||||
```
|
||||
|
||||
### Horror
|
||||
```javascript
|
||||
music: { mood: "Mysterious", energy: 4 }
|
||||
background: { theme: "spooky", variant: "hauntedHouse" }
|
||||
avatarContext: { mode: "FULL_BODY", context: "adventure-hero" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Always use the provided state object** - Don't create global variables for game state.
|
||||
|
||||
2. **Delta time is in seconds** - Multiply speeds by `dt` for frame-independent movement.
|
||||
|
||||
3. **Player position is center-based** - `playerX` and `playerY` are the center of the player.
|
||||
|
||||
4. **Earned accessories always show** - User's crowns, auras, etc. render above costumes.
|
||||
|
||||
5. **Test on mobile** - Touch handlers are provided, but test gesture-based games.
|
||||
|
||||
6. **Use endGame() properly** - It handles score reporting and music fadeout.
|
||||
|
||||
7. **Don't modify the asset library scripts** - Only modify the game logic section.
|
||||
@@ -0,0 +1,905 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,392 @@
|
||||
<!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: #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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div id="ui">Score: <span id="score">0</span></div>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SIMPLE GAME TEMPLATE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// This is a minimal template without the asset library.
|
||||
// Use for simple games that don't need avatars, procedural music, etc.
|
||||
//
|
||||
// HAIKU: Fill in GAME_CONFIG and implement the game functions below.
|
||||
|
||||
const GAME_CONFIG = {
|
||||
title: "<!-- GAME_TITLE -->",
|
||||
|
||||
// Background color or gradient
|
||||
background: {
|
||||
type: "gradient", // "solid", "gradient", or "pattern"
|
||||
colors: ["#1a1a2e", "#16213e", "#0f3460"], // gradient colors
|
||||
// color: "#1a1a2e" // for solid
|
||||
},
|
||||
|
||||
// Player settings
|
||||
player: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
color: "#4CAF50",
|
||||
speed: 300 // pixels per second
|
||||
},
|
||||
|
||||
// Game settings
|
||||
// HAIKU: Add game-specific config here
|
||||
gameplay: {
|
||||
// <!-- GAMEPLAY_CONFIG -->
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SETUP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Resize to fill screen
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// Game state
|
||||
let state = {
|
||||
// Player position
|
||||
playerX: 0,
|
||||
playerY: 0,
|
||||
playerVelX: 0,
|
||||
playerVelY: 0,
|
||||
|
||||
// Score and status
|
||||
score: 0,
|
||||
gameOver: false,
|
||||
paused: false,
|
||||
|
||||
// Timing
|
||||
lastTime: 0,
|
||||
|
||||
// HAIKU: Add game-specific state
|
||||
// <!-- GAME_STATE -->
|
||||
};
|
||||
|
||||
// Input tracking
|
||||
const keys = {};
|
||||
let mouseX = 0, mouseY = 0;
|
||||
let mouseDown = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateScore(points) {
|
||||
state.score += points;
|
||||
document.getElementById('score').textContent = state.score;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.gameOver = true;
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (confirm(`Game Over! Score: ${state.score}\n\nPlay again?`)) {
|
||||
restart();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function restart() {
|
||||
state.score = 0;
|
||||
state.gameOver = false;
|
||||
state.playerX = canvas.width / 2;
|
||||
state.playerY = canvas.height / 2;
|
||||
document.getElementById('score').textContent = '0';
|
||||
initGame();
|
||||
}
|
||||
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function randomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function clamp(val, min, max) {
|
||||
return Math.max(min, Math.min(max, val));
|
||||
}
|
||||
|
||||
function distance(x1, y1, x2, y2) {
|
||||
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
}
|
||||
|
||||
function rectCollide(r1, r2) {
|
||||
return r1.x < r2.x + r2.w && r1.x + r1.w > r2.x &&
|
||||
r1.y < r2.y + r2.h && r1.y + r1.h > r2.y;
|
||||
}
|
||||
|
||||
function circleCollide(c1, c2) {
|
||||
return distance(c1.x, c1.y, c2.x, c2.y) < c1.r + c2.r;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RENDERING HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function drawBackground() {
|
||||
const bg = GAME_CONFIG.background;
|
||||
|
||||
if (bg.type === 'gradient' && bg.colors) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
bg.colors.forEach((color, i) => {
|
||||
gradient.addColorStop(i / (bg.colors.length - 1), color);
|
||||
});
|
||||
ctx.fillStyle = gradient;
|
||||
} else {
|
||||
ctx.fillStyle = bg.color || '#1a1a2e';
|
||||
}
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function drawPlayer() {
|
||||
const p = GAME_CONFIG.player;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.fillRect(
|
||||
state.playerX - p.width / 2,
|
||||
state.playerY - p.height / 2,
|
||||
p.width,
|
||||
p.height
|
||||
);
|
||||
}
|
||||
|
||||
function drawRect(x, y, w, h, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
|
||||
function drawCircle(x, y, r, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawText(text, x, y, color, size, align) {
|
||||
ctx.fillStyle = color || 'white';
|
||||
ctx.font = `${size || 20}px Arial`;
|
||||
ctx.textAlign = align || 'left';
|
||||
ctx.fillText(text, x, y);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INPUT HANDLING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
keys[e.code] = true;
|
||||
if (e.code === 'Escape') {
|
||||
state.paused = !state.paused;
|
||||
if (!state.paused) requestAnimationFrame(gameLoop);
|
||||
}
|
||||
onKeyDown(e.code);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
keys[e.code] = false;
|
||||
onKeyUp(e.code);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
mouseDown = true;
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
onMouseDown(mouseX, mouseY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', e => {
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
onMouseMove(mouseX, mouseY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', e => {
|
||||
mouseDown = false;
|
||||
onMouseUp(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
mouseDown = true;
|
||||
mouseX = e.touches[0].clientX;
|
||||
mouseY = e.touches[0].clientY;
|
||||
onMouseDown(mouseX, mouseY);
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
mouseX = e.touches[0].clientX;
|
||||
mouseY = e.touches[0].clientY;
|
||||
onMouseMove(mouseX, mouseY);
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
mouseDown = false;
|
||||
onMouseUp(mouseX, mouseY);
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOOP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function gameLoop(time) {
|
||||
if (state.gameOver || state.paused) return;
|
||||
|
||||
const dt = (time - state.lastTime) / 1000;
|
||||
state.lastTime = time;
|
||||
|
||||
// Update
|
||||
update(dt);
|
||||
|
||||
// Render
|
||||
drawBackground();
|
||||
render();
|
||||
drawPlayer();
|
||||
renderUI();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOGIC - HAIKU IMPLEMENTS THESE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Initialize game state
|
||||
*/
|
||||
function initGame() {
|
||||
state.playerX = canvas.width / 2;
|
||||
state.playerY = canvas.height / 2;
|
||||
|
||||
// HAIKU: Initialize your game objects
|
||||
// Example:
|
||||
// state.enemies = [];
|
||||
// state.items = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game logic each frame
|
||||
* @param {number} dt - Delta time in seconds
|
||||
*/
|
||||
function update(dt) {
|
||||
// HAIKU: Implement game update logic
|
||||
|
||||
// Example: Arrow key movement
|
||||
const speed = GAME_CONFIG.player.speed;
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) state.playerX -= speed * dt;
|
||||
if (keys['ArrowRight'] || keys['KeyD']) state.playerX += speed * dt;
|
||||
if (keys['ArrowUp'] || keys['KeyW']) state.playerY -= speed * dt;
|
||||
if (keys['ArrowDown'] || keys['KeyS']) state.playerY += speed * dt;
|
||||
|
||||
// Keep in bounds
|
||||
const hw = GAME_CONFIG.player.width / 2;
|
||||
const hh = GAME_CONFIG.player.height / 2;
|
||||
state.playerX = clamp(state.playerX, hw, canvas.width - hw);
|
||||
state.playerY = clamp(state.playerY, hh, canvas.height - hh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render game objects (not player, not background)
|
||||
*/
|
||||
function render() {
|
||||
// HAIKU: Render your game objects
|
||||
// Example:
|
||||
// state.enemies.forEach(e => drawCircle(e.x, e.y, e.r, e.color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render UI elements on canvas
|
||||
*/
|
||||
function renderUI() {
|
||||
// HAIKU: Render any additional UI
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key press
|
||||
*/
|
||||
function onKeyDown(code) {
|
||||
// HAIKU: Handle key press events
|
||||
// if (code === 'Space') jump();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key release
|
||||
*/
|
||||
function onKeyUp(code) {
|
||||
// HAIKU: Handle key release events
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse/touch down
|
||||
*/
|
||||
function onMouseDown(x, y) {
|
||||
// HAIKU: Handle click/tap
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse/touch move
|
||||
*/
|
||||
function onMouseMove(x, y) {
|
||||
// HAIKU: Handle mouse/touch movement
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse/touch up
|
||||
*/
|
||||
function onMouseUp(x, y) {
|
||||
// HAIKU: Handle click/tap release
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// START
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Expose for parent window
|
||||
window.pauseGame = () => { state.paused = true; };
|
||||
window.resumeGame = () => { state.paused = false; state.lastTime = performance.now(); requestAnimationFrame(gameLoop); };
|
||||
window.gameScore = state.score;
|
||||
|
||||
// Initialize and start
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user