RPG
GLM 4.7 Flash GGUF · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 54,254 bytes · SHA-256 660c3d256999
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cave of Echoes - Action Adventure RPG</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a0f;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
#gameContainer {
position: relative;
box-shadow: 0 0 50px rgba(0, 200, 255, 0.2);
border: 3px solid #1a1a2e;
border-radius: 8px;
}
#gameCanvas {
display: block;
border-radius: 5px;
}
#ui {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 15px;
background: linear-gradient(to bottom, rgba(0,0,0,0.9), transparent);
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
}
.ui-section {
pointer-events: auto;
}
#healthDisplay {
display: flex;
gap: 8px;
align-items: center;
}
.heart {
width: 24px;
height: 24px;
background: #ff3366;
clip-path: polygon(50% 0%, 100% 35%, 82% 100%, 50% 75%, 18% 100%, 0% 35%);
transform: scale(1.2);
transition: all 0.3s;
filter: drop-shadow(0 0 5px rgba(255,51,102,0.5));
}
.heart.empty {
background: #333;
filter: drop-shadow(0 0 5px rgba(51,51,51,0.5));
}
#stats {
color: #fff;
font-size: 14px;
text-shadow: 2px 2px 4px #000;
}
#stats div {
margin: 5px 0;
}
#inventory {
display: flex;
gap: 8px;
flex-wrap: wrap;
max-width: 200px;
}
.inventory-slot {
width: 40px;
height: 40px;
background: rgba(0,0,0,0.7);
border: 2px solid #444;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
position: relative;
}
.inventory-slot.ranged {
border-color: #ff9900;
}
#shopPanel, #savePanel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(10, 10, 20, 0.95);
border: 3px solid #00ccff;
border-radius: 15px;
padding: 25px;
z-index: 100;
display: none;
min-width: 350px;
}
#shopPanel.active, #savePanel.active {
display: block;
}
.panel-title {
color: #00ccff;
font-size: 20px;
margin-bottom: 15px;
text-align: center;
text-transform: uppercase;
letter-spacing: 2px;
}
.shop-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin: 8px 0;
background: rgba(0,0,0,0.5);
border-radius: 8px;
border: 1px solid #333;
transition: all 0.2s;
}
.shop-item:hover {
border-color: #00ccff;
background: rgba(0,200,255,0.1);
}
.shop-item.disabled {
opacity: 0.5;
pointer-events: none;
}
.shop-item-name {
color: #fff;
font-weight: bold;
}
.shop-item-cost {
color: #ffcc00;
font-size: 18px;
}
.shop-item .buy-btn {
background: linear-gradient(135deg, #00ccff, #0099cc);
border: none;
color: #000;
padding: 8px 20px;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.shop-item .buy-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 15px rgba(0,200,255,0.5);
}
.shop-item .buy-btn:disabled {
background: #333;
color: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.close-btn {
position: absolute;
top: 10px;
right: 15px;
background: #ff3366;
border: none;
color: #fff;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.close-btn:hover {
background: #ff6699;
transform: rotate(90deg);
}
.save-point-indicator {
position: absolute;
width: 20px;
height: 20px;
background: #00ff88;
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
animation: pulse 1s infinite;
pointer-events: none;
z-index: 10;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.7; }
}
.damage-number {
position: absolute;
color: #ff3366;
font-size: 20px;
font-weight: bold;
text-shadow: 2px 2px 0 #000;
pointer-events: none;
animation: floatUp 1s forwards;
z-index: 50;
}
@keyframes floatUp {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(-40px); opacity: 0; }
}
#levelUp {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: linear-gradient(135deg, rgba(0,0,0,0.95), rgba(50,0,100,0.95));
border: 3px solid #ff9900;
border-radius: 15px;
padding: 30px;
z-index: 200;
display: none;
text-align: center;
}
#levelUp.active {
display: block;
}
#levelUp h2 {
color: #ff9900;
font-size: 28px;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 3px;
}
#levelUp .upgrade-options {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.upgrade-option {
background: rgba(255,255,255,0.1);
border: 2px solid #00ccff;
border-radius: 10px;
padding: 20px;
width: 200px;
cursor: pointer;
transition: all 0.3s;
}
.upgrade-option:hover {
background: rgba(0,200,255,0.2);
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,200,255,0.3);
}
.upgrade-option h3 {
color: #00ccff;
font-size: 16px;
margin-bottom: 10px;
}
.upgrade-option p {
color: #aaa;
font-size: 12px;
line-height: 1.5;
}
.upgrade-option.weapon {
border-color: #ff9900;
}
.upgrade-option.weapon h3 {
color: #ff9900;
}
#message {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
border: 2px solid #00ccff;
border-radius: 8px;
padding: 15px 30px;
color: #fff;
font-size: 16px;
text-align: center;
z-index: 100;
display: none;
}
#message.active {
display: block;
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
color: rgba(255,255,255,0.5);
font-size: 12px;
z-index: 50;
}
#startScreen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #0a0a15 0%, #1a0a2e 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 300;
}
#startScreen h1 {
color: #00ccff;
font-size: 48px;
margin-bottom: 10px;
text-shadow: 0 0 30px rgba(0,200,255,0.5);
text-transform: uppercase;
letter-spacing: 5px;
}
#startScreen p {
color: #aaa;
font-size: 16px;
margin-bottom: 30px;
text-align: center;
max-width: 500px;
line-height: 1.6;
}
#startBtn {
background: linear-gradient(135deg, #00ccff, #0099cc);
border: none;
color: #000;
padding: 15px 50px;
font-size: 20px;
font-weight: bold;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 2px;
}
#startBtn:hover {
transform: scale(1.1);
box-shadow: 0 0 30px rgba(0,200,255,0.6);
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="1000" height="600"></canvas>
<div id="ui">
<div id="healthDisplay"></div>
<div id="stats">
<div>💰 <span id="moneyDisplay">0</span></div>
<div>⚔️ <span id="weaponDisplay">Rusty Sword</span></div>
<div>📦 <span id="itemDisplay">0/5</span></div>
<div>🎯 <span id="progressDisplay">0%</span></div>
</div>
<div id="inventory"></div>
</div>
<div id="shopPanel">
<button class="close-btn" onclick="closePanel('shopPanel')">✕</button>
<div class="panel-title">Item Shop</div>
<div id="shopItems"></div>
</div>
<div id="savePanel">
<button class="close-btn" onclick="closePanel('savePanel')">✕</button>
<div class="panel-title">Save Game</div>
<p style="color: #aaa; text-align: center; margin-bottom: 15px;">Click to save your progress</p>
<button class="buy-btn" style="width: 100%;" onclick="saveGame()">Save Progress</button>
</div>
<div id="levelUp">
<h2>Level Up!</h2>
<p style="color: #aaa; margin-bottom: 20px;">Choose your upgrade:</p>
<div class="upgrade-options" id="upgradeOptions"></div>
</div>
<div id="message"></div>
<div id="controls">
WASD - Move | Mouse - Aim | Click - Attack | Space - Interact
</div>
<div id="startScreen">
<h1>Cave of Echoes</h1>
<p>Explore the mysterious caves, battle monsters, collect treasures, and become a legendary warrior. Upgrade your equipment, discover powerful weapons, and save your progress along the way.</p>
<button id="startBtn" onclick="startGame()">Begin Adventure</button>
</div>
</div>
<script>
// ============ GAME ENGINE ============
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Game State
let gameState = {
running: false,
money: 0,
health: 3,
maxHealth: 5,
experience: 0,
level: 1,
inventory: [],
weapons: {
main: { name: 'Rusty Sword', damage: 15, speed: 600, range: 60, upgraded: false, tier: 1 },
ranged: { unlocked: false, weapon: null, damage: 0, speed: 400, range: 400 }
},
upgrades: {
health: false,
damage: false,
speed: false
},
itemsCollected: 0
};
// Player
let player = {
x: 100,
y: 300,
width: 40,
height: 60,
vx: 0,
vy: 0,
speed: 5,
stamina: 100,
facing: 1,
attacking: false,
attackFrame: 0,
attackCooldown: 0,
invincible: false,
invincibleTimer: 0,
frame: 0,
animTimer: 0
};
// Input
let keys = {};
let mouse = { x: 0, y: 0, down: false };
// World
let platforms = [];
let enemies = [];
let items = [];
let particles = [];
let projectiles = [];
let savePoints = [];
let shopLocation = null;
// Audio Context
let audioCtx = null;
// ============ AUDIO SYSTEM ============
function initAudio() {
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);
switch(type) {
case 'walk':
osc.frequency.setValueAtTime(80, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start();
osc.stop(audioCtx.currentTime + 0.1);
break;
case 'attack':
osc.type = 'square';
osc.frequency.setValueAtTime(200, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
osc.start();
osc.stop(audioCtx.currentTime + 0.15);
break;
case 'hit':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.2);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
break;
case 'pickup':
osc.type = 'sine';
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.2);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
break;
case 'shop':
osc.type = 'square';
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(500, audioCtx.currentTime + 0.3);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
osc.start();
osc.stop(audioCtx.currentTime + 0.3);
break;
case 'levelup':
osc.type = 'triangle';
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
osc.start();
osc.stop(audioCtx.currentTime + 0.15);
break;
case 'ranged':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(300, audioCtx.currentTime + 0.2);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
break;
}
}
// ============ WORLD GENERATION ============
function generateWorld() {
platforms = [];
enemies = [];
items = [];
particles = [];
projectiles = [];
savePoints = [];
// Ground
platforms.push({
x: 0, y: 500, width: 1000, height: 100,
color: '#2d1f3d'
});
// Platforms
const platformData = [
{ x: 150, y: 400, w: 120, h: 20 },
{ x: 320, y: 320, w: 100, h: 20 },
{ x: 480, y: 380, w: 100, h: 20 },
{ x: 650, y: 300, w: 120, h: 20 },
{ x: 800, y: 380, w: 100, h: 20 },
{ x: 250, y: 250, w: 100, h: 20 },
{ x: 550, y: 220, w: 120, h: 20 },
{ x: 700, y: 150, w: 100, h: 20 },
{ x: 400, y: 100, w: 100, h: 20 }
];
platformData.forEach(p => {
platforms.push({
x: p.x, y: p.y, width: p.w, height: p.h,
color: '#3d2f4d'
});
});
// Ceiling
platforms.push({
x: 0, y: 0, width: 1000, height: 50,
color: '#1a1a2e'
});
// Enemies
const enemyData = [
{ x: 200, y: 460, type: 'skeleton', hp: 30 },
{ x: 500, y: 340, type: 'goblin', hp: 25 },
{ x: 750, y: 340, type: 'skeleton', hp: 30 },
{ x: 300, y: 210, type: 'goblin', hp: 25 },
{ x: 600, y: 180, type: 'skeleton', hp: 35 },
{ x: 850, y: 100, type: 'goblin', hp: 25 }
];
enemyData.forEach(e => {
enemies.push({
x: e.x, y: e.y, width: 30, height: 30,
type: e.type,
hp: e.hp,
maxHp: e.hp,
vx: 0, vy: 0,
speed: e.type === 'skeleton' ? 2 : 2.5,
attackCooldown: 0,
attackFrame: 0,
facing: -1
});
});
// Items
const itemData = [
{ x: 200, y: 460, type: 'money', value: 50 },
{ x: 500, y: 340, type: 'money', value: 30 },
{ x: 750, y: 340, type: 'health', value: 1 },
{ x: 300, y: 210, type: 'accessory', item: 'speedPotion', name: 'Speed Potion', value: 30 },
{ x: 600, y: 180, type: 'accessory', item: 'damagePotion', name: 'Damage Boost', value: 50 },
{ x: 850, y: 100, type: 'accessory', item: 'healthPotion', name: 'Health Potion', value: 25 },
{ x: 400, y: 60, type: 'money', value: 100 },
{ x: 150, y: 360, type: 'weapon', weapon: 'main', upgrade: 'damage' },
{ x: 650, y: 260, type: 'money', value: 75 }
];
itemData.forEach(i => {
items.push({
x: i.x, y: i.y, width: 20, height: 20,
type: i.type,
value: i.value,
item: i.item,
name: i.name,
weapon: i.weapon,
upgrade: i.upgrade,
collected: false
});
});
// Save Points
savePoints.push({
x: 300, y: 460, activated: false
});
savePoints.push({
x: 700, y: 460, activated: false
});
// Shop Location
shopLocation = { x: 400, y: 460, radius: 40 };
}
// ============ UPDATE SYSTEMS ============
function updatePlayer() {
if (!gameState.running) return;
// Movement
let moving = false;
if (keys['w'] || keys['arrowup']) { player.vy = -player.speed; moving = true; }
if (keys['s'] || keys['arrowdown']) { player.vy = player.speed; moving = true; }
if (keys['a'] || keys['arrowleft']) { player.vx = -player.speed; player.facing = -1; moving = true; }
if (keys['d'] || keys['arrowright']) { player.vx = player.speed; player.facing = 1; moving = true; }
// Apply movement
player.x += player.vx;
player.y += player.vy;
// Platform collision
player.vx = 0;
player.vy = 0;
platforms.forEach(p => {
// Ground collision
if (player.x + player.width > p.x && player.x < p.x + p.width &&
player.y + player.height > p.y && player.y < p.y + p.height) {
// From above
if (player.y + player.height - player.vy <= p.y + 10) {
player.y = p.y - player.height;
player.vy = 0;
}
}
});
// Boundaries
player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
player.y = Math.max(0, Math.min(canvas.height - player.height, player.y));
// Animation
player.animTimer++;
if (player.animTimer > 8) {
player.frame = (player.frame + 1) % 4;
player.animTimer = 0;
}
// Attack
if (player.attackCooldown > 0) player.attackCooldown--;
if (player.attackFrame > 0) player.attackFrame--;
if (mouse.down && player.attackCooldown === 0 && !player.attacking) {
player.attacking = true;
player.attackFrame = 10;
player.attackCooldown = gameState.weapons.main.speed / 16;
playSound('attack');
// Create attack effect
createAttackEffect();
// Check enemy hits
checkEnemyHit();
}
// Ranged attack
if (keys[' '] && player.attackCooldown === 0 && !player.attacking && gameState.weapons.ranged.weapon) {
player.attacking = true;
player.attackFrame = 15;
player.attackCooldown = gameState.weapons.ranged.speed / 16;
playSound('ranged');
const angle = Math.atan2(mouse.y - (player.y + player.height/2), mouse.x - (player.x + player.width/2));
projectiles.push({
x: player.x + player.width/2,
y: player.y + player.height/2,
vx: Math.cos(angle) * 8,
vy: Math.sin(angle) * 8,
life: 60,
damage: gameState.weapons.ranged.damage,
radius: 6
});
}
// Invincibility
if (player.invincible) {
player.invincibleTimer--;
if (player.invincibleTimer <= 0) {
player.invincible = false;
}
}
}
function updateEnemies() {
enemies.forEach(e => {
// AI - chase player
const dx = player.x - e.x;
const dy = player.y - e.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 300) {
e.x += (dx / dist) * e.speed;
e.y += (dy / dist) * e.speed;
}
// Face player
if (dx > 0) e.facing = 1;
else e.facing = -1;
// Attack player
if (dist < 50 && e.attackCooldown === 0) {
e.attackCooldown = 60;
e.attackFrame = 10;
if (!player.invincible) {
takeDamage(10);
createDamageEffect(player.x, player.y, 10);
playSound('hit');
}
}
if (e.attackCooldown > 0) e.attackCooldown--;
if (e.attackFrame > 0) e.attackFrame--;
});
}
function updateProjectiles() {
projectiles.forEach((p, index) => {
p.x += p.vx;
p.y += p.vy;
p.life--;
// Check enemy hit
enemies.forEach(e => {
const dx = p.x - (e.x + e.width/2);
const dy = p.y - (e.y + e.height/2);
if (Math.sqrt(dx*dx + dy*dy) < 25) {
e.hp -= p.damage;
createDamageEffect(e.x, e.y, p.damage);
playSound('hit');
if (e.hp <= 0) {
enemies.splice(index, 1);
createDeathEffect(e.x, e.y);
gainExperience(25);
}
projectiles.splice(index, 1);
}
});
// Remove if out of bounds or expired
if (p.life <= 0) {
projectiles.splice(index, 1);
}
});
}
function updateParticles() {
particles.forEach((p, index) => {
p.x += p.vx;
p.y += p.vy;
p.life--;
p.vy += 0.3;
if (p.life <= 0) {
particles.splice(index, 1);
}
});
}
function checkEnemyHit() {
const attackRange = 60;
const attackAngle = player.facing === 1 ? 0 : Math.PI;
enemies.forEach((e, index) => {
const dx = e.x - player.x;
const dy = e.y - player.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < attackRange) {
e.hp -= gameState.weapons.main.damage;
createDamageEffect(e.x, e.y, gameState.weapons.main.damage);
playSound('hit');
if (e.hp <= 0) {
enemies.splice(index, 1);
createDeathEffect(e.x, e.y);
gainExperience(25);
gameState.money += 10;
}
}
});
}
function checkItems() {
items.forEach(item => {
if (item.collected) return;
const dx = player.x + player.width/2 - item.x;
const dy = player.y + player.height/2 - item.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 40) {
item.collected = true;
playSound('pickup');
switch(item.type) {
case 'money':
gameState.money += item.value;
showMessage(`+${item.value} Gold!`);
break;
case 'health':
if (gameState.health < gameState.maxHealth) {
gameState.health = Math.min(gameState.maxHealth, gameState.health + item.value);
showMessage('Health Upgraded!');
}
break;
case 'weapon':
upgradeWeapon(item.weapon, item.upgrade);
showMessage('Weapon Upgraded!');
break;
case 'accessory':
const newItem = { name: item.name, type: 'consumable' };
gameState.inventory.push(newItem);
gameState.itemsCollected++;
showMessage(`+ ${item.name}`);
break;
}
}
});
}
function checkInteract() {
// Shop
const shop = shopLocation;
const dx = (player.x + player.width/2) - shop.x;
const dy = (player.y + player.height/2) - shop.y;
if (Math.sqrt(dx*dx + dy*dy) < 60) {
openShop();
}
// Save Points
savePoints.forEach(sp => {
if (!sp.activated) {
const dx = player.x + player.width/2 - sp.x;
const dy = player.y + player.height/2 - sp.y;
if (Math.sqrt(dx*dx + dy*dy) < 40) {
showMessage('Save Point Activated!');
sp.activated = true;
playSound('pickup');
}
}
});
}
function takeDamage(amount) {
gameState.health -= amount;
player.invincible = true;
player.invincibleTimer = 60;
updateUI();
if (gameState.health <= 0) {
gameOver();
}
}
function gainExperience(amount) {
gameState.experience += amount;
const expNeeded = player.level * 100;
if (gameState.experience >= expNeeded) {
gameState.experience -= expNeeded;
player.level++;
playSound('levelup');
showLevelUp();
}
updateUI();
}
function upgradeWeapon(type, upgrade) {
const weapon = gameState.weapons[type];
if (type === 'ranged' && !weapon.unlocked) {
weapon.unlocked = true;
weapon.weapon = {
name: 'Magic Bow',
damage: 20,
speed: 400,
range: 400
};
} else if (weapon.upgraded) {
// Already upgraded
return;
}
if (upgrade === 'damage') {
weapon.damage *= 1.5;
weapon.speed *= 0.9;
} else if (upgrade === 'speed') {
weapon.damage *= 1.1;
weapon.speed *= 0.85;
}
weapon.upgraded = true;
}
// ============ PARTICLE SYSTEM ============
function createAttackEffect() {
for (let i = 0; i < 5; i++) {
particles.push({
x: player.x + player.width/2 + player.facing * 30,
y: player.y + player.height/2,
vx: player.facing * (2 + Math.random() * 2),
vy: (Math.random() - 0.5) * 2,
life: 20,
color: gameState.weapons.main.upgraded ? '#ff9900' : '#00ccff',
size: 4 + Math.random() * 4
});
}
}
function createDamageEffect(x, y, amount) {
for (let i = 0; i < 5; i++) {
particles.push({
x: x + Math.random() * 20,
y: y,
vx: (Math.random() - 0.5) * 4,
vy: -2 - Math.random() * 3,
life: 30,
color: '#ff3366',
size: 3
});
}
const dmgEl = document.createElement('div');
dmgEl.className = 'damage-number';
dmgEl.textContent = amount;
dmgEl.style.left = x + 'px';
dmgEl.style.top = y + 'px';
document.getElementById('gameContainer').appendChild(dmgEl);
setTimeout(() => dmgEl.remove(), 1000);
}
function createDeathEffect(x, y) {
for (let i = 0; i < 15; i++) {
particles.push({
x: x + Math.random() * 20,
y: y + Math.random() * 20,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6,
life: 40,
color: '#ffaa00',
size: 3 + Math.random() * 5
});
}
}
// ============ UI SYSTEM ============
function updateUI() {
// Health
const healthDisplay = document.getElementById('healthDisplay');
healthDisplay.innerHTML = '';
for (let i = 0; i < 5; i++) {
const heart = document.createElement('div');
heart.className = 'heart' + (i >= gameState.health ? ' empty' : '');
healthDisplay.appendChild(heart);
}
// Stats
document.getElementById('moneyDisplay').textContent = gameState.money;
document.getElementById('weaponDisplay').textContent = gameState.weapons.main.name;
document.getElementById('itemDisplay').textContent = `${gameState.itemsCollected}/5`;
const progress = Math.min(100, Math.floor((gameState.experience / (player.level * 100)) * 100 + (gameState.money / 500) * 10));
document.getElementById('progressDisplay').textContent = progress + '%';
}
function updateInventory() {
const invDisplay = document.getElementById('inventory');
invDisplay.innerHTML = '';
const itemEmojis = {
'speedPotion': '⚡',
'damagePotion': '⚔️',
'healthPotion': '❤️'
};
gameState.inventory.slice(-8).forEach(item => {
const slot = document.createElement('div');
slot.className = 'inventory-slot';
slot.innerHTML = itemEmojis[item.name] || '📦';
invDisplay.appendChild(slot);
});
}
function showMessage(text) {
const msg = document.getElementById('message');
msg.textContent = text;
msg.classList.add('active');
setTimeout(() => msg.classList.remove('active'), 2000);
}
function openShop() {
playSound('shop');
const shopPanel = document.getElementById('shopPanel');
const shopItems = document.getElementById('shopItems');
shopItems.innerHTML = '';
const shopItemsList = [
{ name: 'Health Potion', cost: 25, item: 'healthPotion' },
{ name: 'Speed Potion', cost: 30, item: 'speedPotion' },
{ name: 'Damage Potion', cost: 50, item: 'damagePotion' }
];
shopItemsList.forEach(shopItem => {
const hasItem = gameState.inventory.some(i => i.name === shopItem.item);
const itemEl = document.createElement('div');
itemEl.className = 'shop-item' + (hasItem ? ' disabled' : '');
const existingCount = gameState.inventory.filter(i => i.name === shopItem.item).length;
const name = existingCount > 0 ? `${shopItem.name} x${existingCount}` : shopItem.name;
const cost = existingCount > 0 ? '' : `$${shopItem.cost}`;
const btnText = existingCount > 0 ? 'Owned' : 'Buy';
const disabled = hasItem || gameState.money < shopItem.cost;
itemEl.innerHTML = `
<div class="shop-item-name">${name}</div>
<div class="shop-item-cost">${cost}</div>
<button class="buy-btn" ${disabled ? 'disabled' : ''}>${btnText}</button>
`;
if (!disabled) {
itemEl.querySelector('.buy-btn').onclick = () => buyItem(shopItem);
}
shopItems.appendChild(itemEl);
});
shopPanel.classList.add('active');
}
function buyItem(shopItem) {
gameState.money -= shopItem.cost;
gameState.inventory.push({ name: shopItem.item, type: 'consumable' });
gameState.itemsCollected++;
playSound('pickup');
updateUI();
updateInventory();
openShop();
}
function closePanel(panelId) {
document.getElementById(panelId).classList.remove('active');
}
function showLevelUp() {
const levelUp = document.getElementById('levelUp');
const options = document.getElementById('upgradeOptions');
options.innerHTML = `
<div class="upgrade-option" onclick="selectUpgrade('health')">
<h3>❤️ Health Boost</h3>
<p>Gain +1 maximum health container. Requires 10 gold.</p>
</div>
<div class="upgrade-option" onclick="selectUpgrade('damage')">
<h3>⚔️ Damage Upgrade</h3>
<p>Increase all weapon damage by 20%. Requires 25 gold.</p>
</div>
<div class="upgrade-option" onclick="selectUpgrade('speed')">
<h3>👟 Speed Boost</h3>
<p>Increase movement speed by 15%. Requires 15 gold.</p>
</div>
`;
levelUp.classList.add('active');
}
function selectUpgrade(type) {
const costs = { health: 10, damage: 25, speed: 15 };
if (gameState.money >= costs[type]) {
gameState.money -= costs[type];
switch(type) {
case 'health':
if (gameState.health < gameState.maxHealth) {
gameState.maxHealth++;
gameState.health++;
gameState.upgrades.health = true;
}
break;
case 'damage':
gameState.weapons.main.damage *= 1.2;
gameState.upgrades.damage = true;
break;
case 'speed':
player.speed *= 1.15;
gameState.upgrades.speed = true;
break;
}
playSound('levelup');
document.getElementById('levelUp').classList.remove('active');
updateUI();
}
}
// ============ SAVE/LOAD SYSTEM ============
function saveGame() {
const saveData = {
money: gameState.money,
health: gameState.health,
maxHealth: gameState.maxHealth,
level: player.level,
experience: gameState.experience,
inventory: gameState.inventory,
weapons: gameState.weapons,
upgrades: gameState.upgrades,
itemsCollected: gameState.itemsCollected,
player: { x: player.x, y: player.y }
};
localStorage.setItem('caveOfEchoesSave', JSON.stringify(saveData));
showMessage('Game Saved!');
playSound('pickup');
}
function loadGame() {
const saveData = localStorage.getItem('caveOfEchoesSave');
if (saveData) {
const data = JSON.parse(saveData);
gameState.money = data.money;
gameState.health = data.health;
gameState.maxHealth = data.maxHealth;
player.level = data.level;
gameState.experience = data.experience;
gameState.inventory = data.inventory;
gameState.weapons = data.weapons;
gameState.upgrades = data.upgrades;
gameState.itemsCollected = data.itemsCollected;
player.x = data.player.x;
player.y = data.player.y;
showMessage('Game Loaded!');
}
}
// ============ GAME LOOP ============
function gameLoop() {
if (!gameState.running) return;
updatePlayer();
updateEnemies();
updateProjectiles();
updateParticles();
checkItems();
checkInteract();
draw();
updateUI();
requestAnimationFrame(gameLoop);
}
function draw() {
// Clear
ctx.fillStyle = '#0a0a15';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Background gradient
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#0a0a20');
gradient.addColorStop(1, '#1a0a30');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw platforms
platforms.forEach(p => {
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, p.width, p.height);
// Platform border
ctx.strokeStyle = '#4a3a5a';
ctx.lineWidth = 2;
ctx.strokeRect(p.x, p.y, p.width, p.height);
// Platform top highlight
ctx.fillStyle = 'rgba(255,255,255,0.1)';
ctx.fillRect(p.x, p.y, p.width, 3);
});
// Draw save points
savePoints.forEach(sp => {
ctx.fillStyle = sp.activated ? '#00ff88' : '#006633';
ctx.beginPath();
ctx.moveTo(sp.x, sp.y - 15);
ctx.lineTo(sp.x + 10, sp.y);
ctx.lineTo(sp.x - 10, sp.y);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText('S', sp.x, sp.y - 5);
});
// Draw shop
const shop = shopLocation;
ctx.fillStyle = '#ff9900';
ctx.beginPath();
ctx.arc(shop.x, shop.y, shop.radius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'center';
ctx.fillText('SHOP', shop.x, shop.y + 5);
// Draw enemies
enemies.forEach(e => {
// Body
ctx.fillStyle = e.type === 'skeleton' ? '#e0d0b0' : '#4a6b3d';
ctx.fillRect(e.x, e.y, e.width, e.height);
// Eyes
ctx.fillStyle = e.type === 'skeleton' ? '#ff0' : '#ff0';
ctx.beginPath();
ctx.arc(e.x + 8, e.y + 8, 4, 0, Math.PI * 2);
ctx.arc(e.x + 22, e.y + 8, 4, 0, Math.PI * 2);
ctx.fill();
// HP bar
const hpPercent = e.hp / e.maxHp;
ctx.fillStyle = '#333';
ctx.fillRect(e.x, e.y - 10, e.width, 5);
ctx.fillStyle = hpPercent > 0.5 ? '#0f0' : hpPercent > 0.25 ? '#ff0' : '#f00';
ctx.fillRect(e.x, e.y - 10, e.width * hpPercent, 5);
});
// Draw items
items.forEach(item => {
if (item.collected) return;
if (item.type === 'money') {
ctx.fillStyle = '#ffcc00';
ctx.beginPath();
ctx.arc(item.x, item.y, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'center';
ctx.fillText('$', item.x, item.y + 3);
} else if (item.type === 'health') {
ctx.fillStyle = '#ff3366';
ctx.beginPath();
ctx.arc(item.x, item.y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'center';
ctx.fillText('+', item.x, item.y + 3);
} else {
ctx.fillStyle = '#00ccff';
ctx.fillRect(item.x - 6, item.y - 6, 12, 12);
ctx.fillStyle = '#000';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText('?', item.x, item.y + 3);
}
// Glow
const glow = ctx.createRadialGradient(item.x, item.y, 0, item.x, item.y, 15);
glow.addColorStop(0, 'rgba(255,255,255,0.3)');
glow.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(item.x, item.y, 15, 0, Math.PI * 2);
ctx.fill();
});
// Draw player
if (!player.invincible || Math.floor(Date.now() / 100) % 2 === 0) {
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath();
ctx.ellipse(player.x + player.width/2, player.y + player.height - 5, 15, 5, 0, 0, Math.PI * 2);
ctx.fill();
// Body
const weaponColor = gameState.weapons.main.upgraded ? '#ff9900' : '#00ccff';
ctx.fillStyle = weaponColor;
ctx.fillRect(player.x + 10, player.y + 20, 20, 35);
// Head
ctx.fillStyle = '#ffd5b5';
ctx.beginPath();
ctx.arc(player.x + player.width/2, player.y + 15, 12, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(player.x + player.width/2 + player.facing * 4, player.y + 12, 2, 0, Math.PI * 2);
ctx.fill();
// Weapon
ctx.fillStyle = '#888';
if (gameState.weapons.main.upgraded) {
ctx.fillStyle = '#ff9900';
ctx.fillRect(player.x + player.facing * 20, player.y + 25, 35, 6);
ctx.fillStyle = '#ff6600';
ctx.fillRect(player.x + player.facing * 45, player.y + 23, 4, 10);
} else {
ctx.fillRect(player.x + player.facing * 15, player.y + 25, 25, 4);
}
// Legs animation
const legOffset = player.animTimer < 4 ? 3 : 0;
ctx.fillStyle = '#444';
ctx.fillRect(player.x + 12, player.y + 50, 8, 15 + legOffset);
ctx.fillRect(player.x + 20, player.y + 50, 8, 15 - legOffset);
}
// Draw projectiles
projectiles.forEach(p => {
ctx.fillStyle = '#ff9900';
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
// Trail
ctx.strokeStyle = 'rgba(255,153,0,0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x - p.vx * 3, p.y - p.vy * 3);
ctx.stroke();
});
// Draw particles
particles.forEach(p => {
ctx.globalAlpha = p.life / 40;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
}
// ============ GAME STATE ============
function gameOver() {
gameState.running = false;
showMessage('Game Over! Press R to restart');
canvas.addEventListener('keypress', function restart(e) {
if (e.key === 'r' || e.key === 'R') {
canvas.removeEventListener('keypress', restart);
restartGame();
}
});
}
function startGame() {
document.getElementById('startScreen').style.display = 'none';
initAudio();
loadGame();
if (!gameState.running) {
gameState.running = true;
generateWorld();
updateUI();
gameLoop();
}
}
function restartGame() {
// Reset game state
gameState = {
running: true,
money: 0,
health: 3,
maxHealth: 5,
experience: 0,
level: 1,
inventory: [],
weapons: {
main: { name: 'Rusty Sword', damage: 15, speed: 600, range: 60, upgraded: false, tier: 1 },
ranged: { unlocked: false, weapon: null, damage: 0, speed: 400, range: 400 }
},
upgrades: {
health: false,
damage: false,
speed: false
},
itemsCollected: 0
};
player = {
x: 100, y: 300, width: 40, height: 60,
vx: 0, vy: 0,
speed: 5,
stamina: 100,
facing: 1,
attacking: false,
attackFrame: 0,
attackCooldown: 0,
invincible: false,
invincibleTimer: 0,
frame: 0,
animTimer: 0
};
// Clear save
localStorage.removeItem('caveOfEchoesSave');
generateWorld();
updateUI();
gameLoop();
}
// ============ EVENT LISTENERS ============
document.addEventListener('keydown', e => {
keys[e.key] = true;
if (e.key === 'r' || e.key === 'R') {
restartGame();
}
});
document.addEventListener('keyup', e => {
keys[e.key] = false;
});
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
canvas.addEventListener('mousedown', () => mouse.down = true);
canvas.addEventListener('mouseup', () => mouse.down = false);
// Prevent context menu on canvas
canvas.addEventListener('contextmenu', e => e.preventDefault());
// Initialize
updateUI();
</script>
</body>
</html>
<!-- agent-meta {"model":"GLM-4.7-Flash-GGUF","provider":"brigid","persona":"full","sessionId":"76de4e31-9661-4f4f-a8fe-4edd01260a29","tokensIn":20501,"tokensOut":12311,"tokensTotal":32812,"cost":0,"turns":3,"toolCalls":2,"failedToolCalls":0,"timestamp":"2026-08-05T23:15:14.131Z"} -->