576 lines
21 KiB
HTML
576 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Defender Browser Clone - UAT Ready v0.2</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
background: #000;
|
|
color: #0f0;
|
|
font-family: 'Courier New', monospace;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
}
|
|
canvas {
|
|
border: 2px solid #0f0;
|
|
image-rendering: crisp-edges;
|
|
box-shadow: 0 0 20px #0f0;
|
|
}
|
|
.info {
|
|
margin-top: 10px;
|
|
font-size: 14px;
|
|
max-width: 800px;
|
|
text-align: center;
|
|
}
|
|
.controls {
|
|
font-size: 12px;
|
|
margin-top: 5px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1 style="margin-bottom: 5px; text-shadow: 0 0 10px #0f0;">DEFENDER</h1>
|
|
<canvas id="game" width="800" height="600"></canvas>
|
|
<div class="info">
|
|
<p><strong>UAT Ready v0.2</strong> - Core loop + humans/rescue, smart bomb, hyperspace, multiple enemies, waves, audio stubs, polish. Test per docs/REQUIREMENTS.md!</p>
|
|
<div class="controls">
|
|
<strong>Controls:</strong> Arrows/WASD = Thrust | SPACE = Laser | SHIFT = Smart Bomb | H = Hyperspace | P = Pause | R = Restart<br>
|
|
<em>Rescue: Shoot lander carrying human → catch falling human!</em>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const canvas = document.getElementById('game');
|
|
const ctx = canvas.getContext('2d', { alpha: false });
|
|
|
|
// Game constants
|
|
const WIDTH = 800;
|
|
const HEIGHT = 600;
|
|
const WORLD_WIDTH = 4096;
|
|
const GROUND_Y = HEIGHT - 80;
|
|
|
|
// Game state
|
|
let gameState = 'PLAYING';
|
|
let score = 0;
|
|
let lives = 3;
|
|
let wave = 1;
|
|
let smartBombs = 3;
|
|
let cameraX = 0;
|
|
|
|
// Audio context (simple synth)
|
|
let audioCtx;
|
|
function initAudio() {
|
|
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
function playSound(type) {
|
|
if (!audioCtx) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
if (type === 'laser') {
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
|
|
gain.gain.value = 0.1;
|
|
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.1);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.1);
|
|
} else if (type === 'explosion') {
|
|
osc.type = 'square';
|
|
osc.frequency.setValueAtTime(120, audioCtx.currentTime);
|
|
gain.gain.value = 0.3;
|
|
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.4);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.4);
|
|
} else if (type === 'catch') {
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(600, audioCtx.currentTime);
|
|
gain.gain.value = 0.2;
|
|
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.3);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.3);
|
|
}
|
|
}
|
|
|
|
// Player
|
|
let player = {
|
|
x: 200, y: HEIGHT / 2, vx: 0, vy: 0, size: 12, facing: 1
|
|
};
|
|
|
|
// Input
|
|
const keys = {};
|
|
window.addEventListener('keydown', e => {
|
|
keys[e.key] = true;
|
|
if (e.key === ' ' && gameState === 'PLAYING') e.preventDefault();
|
|
if (e.key.toLowerCase() === 'p') togglePause();
|
|
if (e.key.toLowerCase() === 'r' && gameState === 'GAME_OVER') restart();
|
|
if (e.key.toLowerCase() === 'h' && gameState === 'PLAYING') hyperspace();
|
|
if ((e.key === 'Shift' || e.key.toLowerCase() === 'b') && gameState === 'PLAYING' && smartBombs > 0) smartBomb();
|
|
});
|
|
window.addEventListener('keyup', e => keys[e.key] = false);
|
|
|
|
// Bullets
|
|
let bullets = [];
|
|
const MAX_BULLETS = 30;
|
|
|
|
// Humans
|
|
let humans = [];
|
|
function spawnHumans(count) {
|
|
humans = [];
|
|
for (let i = 0; i < count; i++) {
|
|
humans.push({
|
|
x: 300 + i * 250 % (WORLD_WIDTH - 400),
|
|
y: GROUND_Y - 15,
|
|
size: 6,
|
|
carried: false,
|
|
falling: false,
|
|
vy: 0
|
|
});
|
|
}
|
|
}
|
|
|
|
// Enemies
|
|
let enemies = [];
|
|
function spawnWave() {
|
|
enemies = [];
|
|
const numLanders = 4 + wave * 2;
|
|
for (let i = 0; i < numLanders; i++) {
|
|
enemies.push({
|
|
x: cameraX + WIDTH + Math.random() * 300,
|
|
y: 100 + Math.random() * 250,
|
|
vx: (Math.random() - 0.5) * 2,
|
|
vy: (Math.random() - 0.5) * 1.5,
|
|
type: 'lander',
|
|
size: 10,
|
|
targetHuman: null
|
|
});
|
|
}
|
|
// Add a bomber occasionally
|
|
if (wave > 1 && Math.random() > 0.5) {
|
|
enemies.push({
|
|
x: cameraX + WIDTH * 0.6,
|
|
y: 150,
|
|
vx: 2,
|
|
vy: 0,
|
|
type: 'bomber',
|
|
size: 12
|
|
});
|
|
}
|
|
}
|
|
|
|
// Particles
|
|
let particles = [];
|
|
function emitParticles(x, y, count, color = '#ff0', spread = 4) {
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push({
|
|
x, y,
|
|
vx: (Math.random() - 0.5) * spread,
|
|
vy: (Math.random() - 0.5) * spread - 1,
|
|
life: 20 + Math.random() * 20,
|
|
color
|
|
});
|
|
}
|
|
}
|
|
|
|
function togglePause() {
|
|
if (gameState === 'PLAYING') gameState = 'PAUSED';
|
|
else if (gameState === 'PAUSED') gameState = 'PLAYING';
|
|
}
|
|
|
|
function hyperspace() {
|
|
initAudio();
|
|
player.x = cameraX + 50 + Math.random() * (WIDTH - 100);
|
|
player.y = 80 + Math.random() * (GROUND_Y - 160);
|
|
if (Math.random() < 0.15) { // risk
|
|
lives--;
|
|
emitParticles(player.x, player.y, 25, '#f00');
|
|
if (lives <= 0) gameState = 'GAME_OVER';
|
|
} else {
|
|
emitParticles(player.x, player.y, 15, '#0ff');
|
|
}
|
|
}
|
|
|
|
function smartBomb() {
|
|
if (smartBombs <= 0) return;
|
|
initAudio();
|
|
smartBombs--;
|
|
emitParticles(player.x, player.y - 50, 40, '#ff0', 8);
|
|
playSound('explosion');
|
|
// Clear on-screen enemies
|
|
for (let i = enemies.length - 1; i >= 0; i--) {
|
|
const e = enemies[i];
|
|
const sx = e.x - cameraX;
|
|
if (sx > -50 && sx < WIDTH + 50) {
|
|
enemies.splice(i, 1);
|
|
score += 100;
|
|
emitParticles(e.x, e.y, 15, '#f80');
|
|
}
|
|
}
|
|
}
|
|
|
|
function restart() {
|
|
score = 0;
|
|
lives = 3;
|
|
wave = 1;
|
|
smartBombs = 3;
|
|
cameraX = 0;
|
|
player.x = 200; player.y = HEIGHT / 2; player.vx = player.vy = 0;
|
|
bullets = []; particles = [];
|
|
spawnHumans(8);
|
|
spawnWave();
|
|
gameState = 'PLAYING';
|
|
}
|
|
|
|
function getTerrainHeight(worldX) {
|
|
const x = worldX % WORLD_WIDTH;
|
|
return GROUND_Y + Math.sin(x / 80) * 25 + Math.sin(x / 30) * 12 + Math.sin(x / 12) * 6;
|
|
}
|
|
|
|
function update() {
|
|
if (gameState !== 'PLAYING') return;
|
|
initAudio();
|
|
|
|
// Player physics
|
|
const thrust = 0.28;
|
|
const maxSpeed = 6.5;
|
|
if (keys['ArrowLeft'] || keys['a'] || keys['A']) { player.vx -= thrust; player.facing = -1; }
|
|
if (keys['ArrowRight'] || keys['d'] || keys['D']) { player.vx += thrust; player.facing = 1; }
|
|
if (keys['ArrowUp'] || keys['w'] || keys['W']) player.vy -= thrust;
|
|
if (keys['ArrowDown'] || keys['s'] || keys['S']) player.vy += thrust;
|
|
|
|
player.vx *= 0.95;
|
|
player.vy *= 0.95;
|
|
player.vx = Math.max(-maxSpeed, Math.min(maxSpeed, player.vx));
|
|
player.vy = Math.max(-maxSpeed, Math.min(maxSpeed, player.vy));
|
|
|
|
player.x += player.vx;
|
|
player.y += player.vy;
|
|
|
|
if (player.x < 0) player.x += WORLD_WIDTH;
|
|
if (player.x > WORLD_WIDTH) player.x -= WORLD_WIDTH;
|
|
|
|
const targetCam = player.x - WIDTH * 0.38;
|
|
cameraX += (targetCam - cameraX) * 0.12;
|
|
|
|
if (player.y < 60) player.y = 60;
|
|
const terrainAtPlayer = getTerrainHeight(player.x);
|
|
if (player.y > terrainAtPlayer - 8) {
|
|
player.y = terrainAtPlayer - 8;
|
|
player.vy = Math.min(0, player.vy * 0.4);
|
|
}
|
|
|
|
// Fire
|
|
if (keys[' '] && bullets.length < MAX_BULLETS) {
|
|
const bx = player.x + player.facing * 18;
|
|
bullets.push({x: bx, y: player.y, vx: player.facing * 13 + player.vx * 0.4, vy: 0, life: 50});
|
|
keys[' '] = false;
|
|
playSound('laser');
|
|
emitParticles(bx, player.y, 4, '#0ff');
|
|
}
|
|
|
|
// Update bullets
|
|
for (let i = bullets.length - 1; i >= 0; i--) {
|
|
const b = bullets[i];
|
|
b.x += b.vx; b.y += b.vy; b.life--;
|
|
if (b.life <= 0 || b.x < cameraX - 100 || b.x > cameraX + WIDTH + 100) bullets.splice(i, 1);
|
|
}
|
|
|
|
// Update humans
|
|
for (let h of humans) {
|
|
if (h.falling) {
|
|
h.vy += 0.18;
|
|
h.y += h.vy;
|
|
if (h.y > getTerrainHeight(h.x) - 5) {
|
|
h.y = getTerrainHeight(h.x) - 5;
|
|
h.falling = false;
|
|
h.vy = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update enemies
|
|
for (let e of enemies) {
|
|
if (e.type === 'lander') {
|
|
// Seek human or player
|
|
let target = {x: player.x, y: player.y};
|
|
for (let h of humans) {
|
|
if (!h.carried && !h.falling && Math.abs(h.x - e.x) < Math.abs(target.x - e.x)) {
|
|
target = h;
|
|
}
|
|
}
|
|
e.vx += (target.x - e.x) * 0.008;
|
|
e.vy += (target.y - e.y) * 0.012;
|
|
// Abduct?
|
|
for (let hi = 0; hi < humans.length; hi++) {
|
|
const h = humans[hi];
|
|
if (!h.carried && !h.falling && Math.hypot(e.x - h.x, e.y - h.y) < 18) {
|
|
h.carried = true;
|
|
e.targetHuman = h;
|
|
break;
|
|
}
|
|
}
|
|
if (e.targetHuman) {
|
|
const h = e.targetHuman;
|
|
h.x = e.x; h.y = e.y - 12;
|
|
if (h.y < 40) {
|
|
// Lost human
|
|
humans = humans.filter(hh => hh !== h);
|
|
e.targetHuman = null;
|
|
score = Math.max(0, score - 200);
|
|
}
|
|
}
|
|
} else if (e.type === 'bomber') {
|
|
e.x += e.vx;
|
|
if (Math.random() < 0.03) {
|
|
// Drop bomb (simple projectile)
|
|
enemies.push({x: e.x, y: e.y + 10, vx: 0, vy: 3.5, type: 'bomb', size: 5});
|
|
}
|
|
}
|
|
// General movement clamp
|
|
e.vx *= 0.97; e.vy *= 0.97;
|
|
e.x += e.vx; e.y += e.vy;
|
|
if (e.y < 70) e.vy = 1;
|
|
if (e.y > GROUND_Y - 25) e.vy = -1.5;
|
|
}
|
|
|
|
// Collisions: bullets vs enemies/humans
|
|
for (let i = bullets.length - 1; i >= 0; i--) {
|
|
const b = bullets[i];
|
|
for (let j = enemies.length - 1; j >= 0; j--) {
|
|
const e = enemies[j];
|
|
if (Math.hypot(b.x - e.x, b.y - e.y) < e.size + 5) {
|
|
bullets.splice(i, 1);
|
|
enemies.splice(j, 1);
|
|
score += e.type === 'lander' ? 150 : 250;
|
|
playSound('explosion');
|
|
emitParticles(e.x, e.y, 18, '#f80');
|
|
if (e.targetHuman) {
|
|
const h = e.targetHuman;
|
|
h.carried = false;
|
|
h.falling = true;
|
|
h.vy = -1.5;
|
|
e.targetHuman = null;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Player catch humans
|
|
for (let hi = humans.length - 1; hi >= 0; hi--) {
|
|
const h = humans[hi];
|
|
if (h.falling && Math.hypot(player.x - h.x, player.y - h.y) < 18) {
|
|
// Caught!
|
|
humans.splice(hi, 1);
|
|
score += 800;
|
|
playSound('catch');
|
|
emitParticles(player.x, player.y, 10, '#0f0');
|
|
// Respawn human later or count rescued
|
|
if (humans.length < 4) spawnHumans(2); // replenish
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Player-enemy collisions
|
|
for (let j = enemies.length - 1; j >= 0; j--) {
|
|
const e = enemies[j];
|
|
if (Math.hypot(player.x - e.x, player.y - e.y) < player.size + e.size * 0.8) {
|
|
enemies.splice(j, 1);
|
|
lives--;
|
|
playSound('explosion');
|
|
emitParticles(player.x, player.y, 25, '#f00');
|
|
if (lives <= 0) gameState = 'GAME_OVER';
|
|
else {
|
|
player.x = cameraX + 180;
|
|
player.y = HEIGHT / 2;
|
|
player.vx = player.vy = 0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update particles
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
const p = particles[i];
|
|
p.x += p.vx; p.y += p.vy; p.vy += 0.12; p.life--;
|
|
if (p.life <= 0) particles.splice(i, 1);
|
|
}
|
|
|
|
// Wave advance (simple)
|
|
if (enemies.length === 0 && humans.length > 0) {
|
|
wave++;
|
|
smartBombs = Math.min(5, smartBombs + 1);
|
|
spawnWave();
|
|
spawnHumans(6 + wave);
|
|
score += 1000;
|
|
}
|
|
|
|
// Respawn logic stub
|
|
if (Math.random() < 0.008 && enemies.length < 8) {
|
|
spawnWave(); // occasional extra
|
|
}
|
|
}
|
|
|
|
function draw() {
|
|
ctx.fillStyle = '#000011';
|
|
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
|
|
// Stars
|
|
ctx.fillStyle = '#fff';
|
|
for (let i = 0; i < 90; i++) {
|
|
const sx = (i * 37 + (cameraX * 0.1 | 0)) % WIDTH;
|
|
const sy = (i * 23) % (GROUND_Y - 80);
|
|
ctx.fillRect(sx, sy, 1.5, 1.5);
|
|
}
|
|
|
|
// Terrain
|
|
ctx.strokeStyle = '#0a0';
|
|
ctx.lineWidth = 4;
|
|
ctx.beginPath();
|
|
let first = true;
|
|
for (let sx = 0; sx <= WIDTH + 40; sx += 6) {
|
|
const wx = cameraX + sx;
|
|
const ty = getTerrainHeight(wx);
|
|
if (first) { ctx.moveTo(sx, ty); first = false; } else { ctx.lineTo(sx, ty); }
|
|
}
|
|
ctx.lineTo(WIDTH + 40, HEIGHT);
|
|
ctx.lineTo(0, HEIGHT);
|
|
ctx.closePath();
|
|
ctx.fillStyle = '#030';
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// Humans
|
|
ctx.fillStyle = '#88f';
|
|
for (let h of humans) {
|
|
const sx = h.x - cameraX;
|
|
if (sx < -30 || sx > WIDTH + 30) continue;
|
|
ctx.fillRect(sx - 3, h.y - 8, 6, 10);
|
|
ctx.fillRect(sx - 5, h.y - 4, 10, 4); // simple body
|
|
}
|
|
|
|
// Enemies
|
|
for (let e of enemies) {
|
|
const sx = e.x - cameraX;
|
|
if (sx < -60 || sx > WIDTH + 60) continue;
|
|
if (e.type === 'lander' || e.type === 'bomber') {
|
|
ctx.fillStyle = e.type === 'lander' ? '#f80' : '#a0f';
|
|
ctx.beginPath();
|
|
ctx.arc(sx, e.y, e.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillRect(sx - 6, e.y + 6, 12, 5);
|
|
} else if (e.type === 'bomb') {
|
|
ctx.fillStyle = '#f44';
|
|
ctx.fillRect(sx - 3, e.y - 3, 6, 8);
|
|
}
|
|
}
|
|
|
|
// Bullets
|
|
ctx.fillStyle = '#0ff';
|
|
for (let b of bullets) {
|
|
const sx = b.x - cameraX;
|
|
ctx.fillRect(sx - 3, b.y - 1.5, 9, 3);
|
|
}
|
|
|
|
// Player ship
|
|
const sx = player.x - cameraX;
|
|
ctx.save();
|
|
ctx.translate(sx, player.y);
|
|
if (player.facing < 0) ctx.scale(-1, 1);
|
|
ctx.fillStyle = '#0f0';
|
|
ctx.beginPath();
|
|
ctx.moveTo(14, 0);
|
|
ctx.lineTo(-10, -9);
|
|
ctx.lineTo(-6, 0);
|
|
ctx.lineTo(-10, 9);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
|
|
// Thrust
|
|
if (Math.abs(player.vx) > 0.5 || Math.abs(player.vy) > 0.5) {
|
|
ctx.fillStyle = '#fa0';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-11, 0);
|
|
ctx.lineTo(-22, Math.random() * 4 - 2);
|
|
ctx.lineTo(-22, Math.random() * 4 - 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
|
|
// Particles
|
|
for (let p of particles) {
|
|
ctx.globalAlpha = Math.max(0.2, p.life / 35);
|
|
ctx.fillStyle = p.color;
|
|
ctx.fillRect(p.x - cameraX, p.y, 2.5, 2.5);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
|
|
// HUD
|
|
ctx.fillStyle = '#0f0';
|
|
ctx.font = 'bold 20px monospace';
|
|
ctx.fillText(`SCORE ${score.toString().padStart(6,'0')}`, 25, 35);
|
|
ctx.fillText(`WAVE ${wave}`, WIDTH - 130, 35);
|
|
ctx.fillText(`BOMBS ${smartBombs}`, WIDTH - 130, 62);
|
|
// Lives
|
|
for (let l = 0; l < lives; l++) {
|
|
ctx.save();
|
|
ctx.translate(WIDTH - 40 - l*26, 35);
|
|
ctx.scale(0.65, 0.65);
|
|
ctx.fillStyle = '#0f0';
|
|
ctx.beginPath(); ctx.moveTo(14,0); ctx.lineTo(-10,-9); ctx.lineTo(-6,0); ctx.lineTo(-10,9); ctx.closePath(); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
// Radar
|
|
ctx.fillStyle = '#112';
|
|
ctx.fillRect(0, 0, WIDTH, 22);
|
|
ctx.strokeStyle = '#0f0'; ctx.lineWidth = 1; ctx.strokeRect(0,0,WIDTH,22);
|
|
ctx.fillStyle = '#0f0';
|
|
ctx.fillRect( ((player.x % WORLD_WIDTH) / WORLD_WIDTH) * WIDTH - 3, 8, 7, 6);
|
|
ctx.fillStyle = '#f80';
|
|
for (let e of enemies) {
|
|
const rx = ((e.x % WORLD_WIDTH) / WORLD_WIDTH) * WIDTH;
|
|
ctx.fillRect(rx - 2, 9, 4, 4);
|
|
}
|
|
|
|
// States
|
|
if (gameState === 'PAUSED') {
|
|
ctx.fillStyle = 'rgba(0,20,0,0.75)';
|
|
ctx.fillRect(0,0,WIDTH,HEIGHT);
|
|
ctx.fillStyle = '#0f0'; ctx.font = 'bold 52px monospace';
|
|
ctx.fillText('PAUSED', WIDTH/2-110, HEIGHT/2);
|
|
}
|
|
if (gameState === 'GAME_OVER') {
|
|
ctx.fillStyle = 'rgba(40,0,0,0.8)';
|
|
ctx.fillRect(0,0,WIDTH,HEIGHT);
|
|
ctx.fillStyle = '#f66'; ctx.font = 'bold 52px monospace';
|
|
ctx.fillText('GAME OVER', WIDTH/2-165, HEIGHT/2-30);
|
|
ctx.fillStyle = '#0f0'; ctx.font = '22px monospace';
|
|
ctx.fillText(`FINAL: ${score}`, WIDTH/2-80, HEIGHT/2+20);
|
|
ctx.fillText('R to Restart', WIDTH/2-75, HEIGHT/2+55);
|
|
}
|
|
|
|
ctx.fillStyle = '#555'; ctx.font = '11px monospace';
|
|
ctx.fillText(`cam:${cameraX|0} lives:${lives} wave:${wave}`, 20, HEIGHT-12);
|
|
}
|
|
|
|
function gameLoop() {
|
|
update();
|
|
draw();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Init
|
|
console.log('%c[Defender UAT v0.2] Ready! Test flight, rescue, bombs, hyperspace, waves. Feedback to issues.', 'color:#0f0; font-weight:bold');
|
|
spawnHumans(8);
|
|
spawnWave();
|
|
emitParticles(player.x, player.y, 12, '#0f0');
|
|
gameLoop();
|
|
</script>
|
|
</body>
|
|
</html> |