RPG
thinkingcap qwen3.6 27b · middle

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 59,522 bytes · SHA-256 5bf2a5fa020d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Realm of Echoes - Action Adventure RPG</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; font-family: 'Segoe UI', sans-serif; user-select: none; }
canvas { display: block; image-rendering: pixelated; }
#ui-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#hud { position: absolute; top: 12px; left: 12px; display: flex; gap: 8px; align-items: center; }
.heart { width: 24px; height: 24px; filter: drop-shadow(0 0 3px rgba(255,50,50,0.6)); transition: all 0.3s; }
.heart.empty { opacity: 0.25; filter: none; }
#gold-display { color: #ffd700; font-size: 18px; font-weight: bold; text-shadow: 0 0 8px rgba(255,215,0,0.5); margin-left: 12px; }
#weapon-name { color: #aaa; font-size: 13px; margin-left: 8px; }
#inventory-btn, #map-btn { position: absolute; top: 12px; right: 12px; pointer-events: all; background: rgba(0,0,0,0.7); border: 1px solid #555; color: #ccc; padding: 8px 14px; cursor: pointer; font-size: 13px; border-radius: 4px; }
#map-btn { right: 120px; }
#inventory-btn:hover, #map-btn:hover { background: rgba(50,50,80,0.9); border-color: #88f; color: #fff; }
#minimap { position: absolute; bottom: 12px; right: 12px; border: 2px solid #333; border-radius: 4px; opacity: 0.85; }
#dialog-box { position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%); background: rgba(10,10,20,0.92); border: 2px solid #446; color: #ddd; padding: 16px 24px; max-width: 500px; text-align: center; font-size: 15px; line-height: 1.5; display: none; border-radius: 8px; }
#shop-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(10,10,20,0.96); border: 2px solid #c9a030; color: #ddd; padding: 24px; width: 420px; display: none; border-radius: 8px; pointer-events: all; }
#shop-panel h2 { text-align: center; color: #ffd700; margin-bottom: 16px; font-size: 20px; }
.shop-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; margin: 4px 0; background: rgba(255,255,255,0.05); border-radius: 4px; cursor: pointer; transition: background 0.2s; }
.shop-item:hover { background: rgba(255,215,0,0.15); }
.shop-item .name { font-size: 14px; }
.shop-item .cost { color: #ffd700; font-size: 13px; }
#inv-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(10,10,20,0.96); border: 2px solid #4488cc; color: #ddd; padding: 20px; width: 380px; display: none; border-radius: 8px; pointer-events: all; }
#inv-panel h2 { text-align: center; margin-bottom: 12px; color: #6af; font-size: 18px; }
.inv-item { padding: 6px 10px; margin: 3px 0; background: rgba(255,255,255,0.04); border-radius: 4px; font-size: 13px; display: flex; justify-content: space-between; }
.inv-item .use-btn { color: #6f6; cursor: pointer; margin-left: 8px; }
#title-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a0f 70%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#title-screen h1 { font-size: 48px; color: #c9a030; text-shadow: 0 0 30px rgba(201,160,48,0.5); margin-bottom: 8px; letter-spacing: 4px; }
#title-screen .subtitle { color: #778; font-size: 16px; margin-bottom: 40px; }
#start-btn { background: linear-gradient(135deg, #2a1a0a, #4a3020); border: 2px solid #c9a030; color: #ffd700; padding: 14px 48px; font-size: 18px; cursor: pointer; border-radius: 6px; letter-spacing: 2px; transition: all 0.3s; }
#start-btn:hover { background: linear-gradient(135deg, #4a3020, #6a5040); box-shadow: 0 0 20px rgba(201,160,48,0.4); transform: scale(1.05); }
#controls-hint { color: #556; font-size: 13px; margin-top: 30px; text-align: center; line-height: 1.8; }
#game-over-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(20,0,0,0.9); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#game-over-screen h1 { color: #f44; font-size: 48px; margin-bottom: 20px; }
#restart-btn { background: #333; border: 2px solid #f44; color: #f66; padding: 12px 36px; font-size: 16px; cursor: pointer; border-radius: 6px; margin-top: 20px; }
#victory-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,10,30,0.95); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#victory-screen h1 { color: #ffd700; font-size: 42px; margin-bottom: 16px; text-shadow: 0 0 30px rgba(255,215,0,0.5); }
#notification { position: absolute; top: 60px; left: 50%; transform: translateX(-50%); color: #ffd700; font-size: 14px; text-shadow: 0 0 10px rgba(255,215,0,0.5); opacity: 0; transition: opacity 0.3s; pointer-events: none; }
#xp-bar-container { position: absolute; top: 44px; left: 12px; width: 160px; height: 8px; background: rgba(0,0,0,0.5); border-radius: 4px; overflow: hidden; }
#xp-bar { height: 100%; background: linear-gradient(90deg, #4a2, #6d3); width: 0%; transition: width 0.4s; }
#level-display { color: #8c8; font-size: 12px; margin-left: 8px; line-height: 8px; }
</style>
</head>
<body>
<div id="title-screen">
<h1>REALM OF ECHOES</h1>
<div class="subtitle">An Action-Adventure RPG</div>
<button id="start-btn" onclick="startGame()">BEGIN QUEST</button>
<div id="controls-hint">
WASD / Arrow Keys — Move | SPACE — Attack<br>
E — Interact | I — Inventory | M — Map<br>
1-3 — Use Items | Ranged: Hold SHIFT + Click
</div>
</div>
<div id="game-over-screen">
<h1>YOU HAVE FALLEN</h1>
<p style="color:#aaa;">The realm claims another soul...</p>
<button id="restart-btn" onclick="location.reload()">TRY AGAIN</button>
</div>
<div id="victory-screen">
<h1>VICTORY!</h1>
<p style="color:#ccc;max-width:400px;text-align:center;line-height:1.6;">The Shadow Lord has been vanquished.<br>The realm is free once more.</p>
<button id="restart-btn" onclick="location.reload()" style="border-color:#ffd700;color:#ffd700;margin-top:24px;">PLAY AGAIN</button>
</div>
<canvas id="game"></canvas>
<div id="ui-overlay">
<div id="hud">
<svg class="heart" viewBox="0 0 24 24"><path fill="#e33" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
<svg class="heart" viewBox="0 0 24 24"><path fill="#e33" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
<svg class="heart" viewBox="0 0 24 24"><path fill="#e33" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
<span id="gold-display">🪙 0</span>
<span id="weapon-name">Iron Sword</span>
</div>
<div id="xp-bar-container"><div id="xp-bar"></div></div>
<span id="level-display" style="position:absolute;top:46px;left:180px;">Lv 1</span>
<button id="map-btn" onclick="toggleMap()">🗺 Map</button>
<button id="inventory-btn" onclick="toggleInventory()">🎒 Inventory</button>
<canvas id="minimap" width="120" height="90"></canvas>
<div id="dialog-box"></div>
<div id="notification"></div>
<div id="shop-panel">
<h2>⚒ Merchant's Wares</h2>
<div id="shop-items"></div>
<p style="text-align:center;margin-top:12px;color:#888;font-size:12px;">Press ESC to close</p>
</div>
<div id="inv-panel">
<h2>🎒 Inventory</h2>
<div id="inv-items"></div>
<p style="text-align:center;margin-top:12px;color:#888;font-size:12px;">Press I or ESC to close</p>
</div>
</div>
<script>
// ==================== AUDIO ENGINE ====================
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() { if (!audioCtx) audioCtx = new AudioCtx(); }
function playSound(type, vol=0.3) {
if (!audioCtx) return;
const now = audioCtx.currentTime;
const g = audioCtx.createGain();
g.connect(audioCtx.destination);
g.gain.setValueAtTime(vol, now);
switch(type) {
case 'walk': {
const o = audioCtx.createOscillator();
o.type = 'sine'; o.frequency.value = 80 + Math.random()*40;
g.gain.exponentialRampToValueAtTime(0.01, now+0.06);
o.connect(g); o.start(now); o.stop(now+0.06);
break;
}
case 'attack': {
const o = audioCtx.createOscillator();
o.type = 'sawtooth'; o.frequency.setValueAtTime(300, now);
o.frequency.exponentialRampToValueAtTime(80, now+0.15);
g.gain.exponentialRampToValueAtTime(0.01, now+0.15);
o.connect(g); o.start(now); o.stop(now+0.15);
break;
}
case 'hit': {
const buf = audioCtx.createBuffer(1, audioCtx.sampleRate*0.1, audioCtx.sampleRate);
const d = buf.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*4);
const s = audioCtx.createBufferSource(); s.buffer=buf;
g.gain.exponentialRampToValueAtTime(0.01, now+0.1);
s.connect(g); s.start(now);
break;
}
case 'pickup': {
const o = audioCtx.createOscillator();
o.type='sine'; o.frequency.setValueAtTime(400,now);
o.frequency.exponentialRampToValueAtTime(800,now+0.15);
g.gain.exponentialRampToValueAtTime(0.01,now+0.2);
o.connect(g); o.start(now); o.stop(now+0.2);
break;
}
case 'heal': {
const o = audioCtx.createOscillator();
o.type='sine'; o.frequency.setValueAtTime(500,now);
o.frequency.exponentialRampToValueAtTime(900,now+0.3);
g.gain.exponentialRampToValueAtTime(0.01,now+0.4);
o.connect(g); o.start(now); o.stop(now+0.4);
break;
}
case 'shop': {
const o = audioCtx.createOscillator();
o.type='triangle'; o.frequency.setValueAtTime(600,now);
o.frequency.exponentialRampToValueAtTime(300,now+0.12);
g.gain.exponentialRampToValueAtTime(0.01,now+0.15);
o.connect(g); o.start(now); o.stop(now+0.15);
break;
}
case 'save': {
[400,500,600].forEach((f,i)=>{
const o=audioCtx.createOscillator();o.type='sine';o.frequency.value=f;
const g2=audioCtx.createGain();g2.gain.setValueAtTime(0.15,now+i*0.1);
g2.gain.exponentialRampToValueAtTime(0.01,now+i*0.1+0.2);
o.connect(g2);g2.connect(audioCtx.destination);o.start(now+i*0.1);o.stop(now+i*0.1+0.2);
});
break;
}
case 'ranged': {
const o=audioCtx.createOscillator();o.type='sine';o.frequency.setValueAtTime(900,now);
o.frequency.exponentialRampToValueAtTime(200,now+0.1);
g.gain.exponentialRampToValueAtTime(0.01,now+0.12);
o.connect(g);o.start(now);o.stop(now+0.12);
break;
}
case 'levelup': {
[523,659,784].forEach((f,i)=>{
const o=audioCtx.createOscillator();o.type='triangle';o.frequency.value=f;
const g2=audioCtx.createGain();g2.gain.setValueAtTime(0.2,now+i*0.15);
g2.gain.exponentialRampToValueAtTime(0.01,now+i*0.15+0.3);
o.connect(g2);g2.connect(audioCtx.destination);o.start(now+i*0.15);o.stop(now+i*0.15+0.3);
});
break;
}
case 'death': {
const o=audioCtx.createOscillator();o.type='sawtooth';o.frequency.setValueAtTime(200,now);
o.frequency.exponentialRampToValueAtTime(40,now+1);
g.gain.exponentialRampToValueAtTime(0.01,now+1);
o.connect(g);o.start(now);o.stop(now+1);
break;
}
}
}
// ==================== CANVAS SETUP ====================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const minimapCanvas = document.getElementById('minimap');
const mctx = minimapCanvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// ==================== CONSTANTS ====================
const TILE = 32;
const ROOM_W = 20, ROOM_H = 15;
const PLAYER_SIZE = 24;
const ENEMY_SIZE = 22;
// Tile types
const T = { EMPTY:0, WALL:1, FLOOR:2, WATER:3, GRASS:4, DOOR:5, SAVEPOINT:6, SHOP:7, CHEST:8, BRIDGE:9 };
// ==================== MAP DATA ====================
// 5 rooms forming a progression path
const rooms = [
// Room 0: Starting Village (peaceful)
{ name:"Village", bg:'#2a3a1a', floor:'#4a6a3a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:false },
// Room 1: Dark Forest
{ name:"Dark Forest", bg:'#1a2a0a', floor:'#2a4a1a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:false },
// Room 2: Crystal Caverns (mid-game - ranged weapon here)
{ name:"Crystal Caverns", bg:'#0a1a2a', floor:'#1a3a5a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:true },
// Room 3: Merchant's Keep (shop room)
{ name:"Merchant's Keep", bg:'#2a2010', floor:'#4a3a20', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:false },
// Room 4: Shadow Fortress (final area)
{ name:"Shadow Fortress", bg:'#1a0a1a', floor:'#2a1a2a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:true, midgame:false }
];
function buildMaps() {
// Room 0: Village - open area with some trees/walls
rooms[0].walls = [];
for(let x=0;x<ROOM_W;x++){rooms[0].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[0].walls[x][y]=T.FLOOR;}
// Border walls
for(let x=0;x<ROOM_W;x++){rooms[0].walls[x][0]=T.WALL;rooms[0].walls[x][ROOM_H-1]=T.WALL;}
for(let y=0;y<ROOM_H;y++){rooms[0].walls[0][y]=T.WALL;rooms[0].walls[ROOM_W-1][y]=T.WALL;}
// Interior walls (decorative)
[[5,3],[6,3],[7,3],[5,4],[5,5],[12,8],[13,8],[14,8],[12,9],[12,10]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[0].walls[x][y]=T.WALL;});
// Door to room 1 (right side)
rooms[0].walls[ROOM_W-1][7]=T.DOOR;
rooms[0].exits = { right:1 };
// Save point
rooms[0].items.push({x:3,y:3,type:'savepoint',collected:false});
// Gold pickup
rooms[0].items.push({x:8,y:5,type:'gold',amount:15,collected:false});
// Health potion
rooms[0].items.push({x:14,y:3,type:'healthPotion',collected:false});
// Room 1: Dark Forest
rooms[1].walls = [];
for(let x=0;x<ROOM_W;x++){rooms[1].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[1].walls[x][y]=T.FLOOR;}
for(let x=0;x<ROOM_W;x++){rooms[1].walls[x][0]=T.WALL;rooms[1].walls[x][ROOM_H-1]=T.WALL;}
for(let y=0;y<ROOM_H;y++){rooms[1].walls[0][y]=T.WALL;rooms[1].walls[ROOM_W-1][y]=T.WALL;}
// Dense trees (walls)
[[3,2],[4,2],[5,2],[3,3],[8,5],[9,5],[10,5],[8,6],[15,3],[16,3],[17,3],[15,4],[15,5],
[4,10],[5,10],[6,10],[4,11],[12,10],[13,10],[14,10]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[1].walls[x][y]=T.WALL;});
// Doors: left to room 0, right to room 2, down to room 3
rooms[1].walls[0][7]=T.DOOR;
rooms[1].walls[ROOM_W-1][7]=T.DOOR;
rooms[1].walls[10][ROOM_H-1]=T.DOOR;
rooms[1].exits = { left:0, right:2, down:3 };
// Enemies
rooms[1].enemies.push({x:6,y:4,type:'slime',hp:8,maxHp:8,dmg:2,speed:0.8,xp:15,gold:5});
rooms[1].enemies.push({x:13,y:7,type:'slime',hp:8,maxHp:8,dmg:2,speed:0.8,xp:15,gold:5});
rooms[1].enemies.push({x:9,y:11,type:'wolf',hp:14,maxHp:14,dmg:3,speed:1.2,xp:25,gold:10});
// Items
rooms[1].items.push({x:7,y:8,type:'gold',amount:20,collected:false});
rooms[1].items.push({x:16,y:9,type:'shield',collected:false}); // Accessory 1
rooms[1].items.push({x:4,y:13,type:'healthPotion',collected:false});
// Room 2: Crystal Caverns (mid-game)
rooms[2].walls = [];
for(let x=0;x<ROOM_W;x++){rooms[2].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[2].walls[x][y]=T.FLOOR;}
for(let x=0;x<ROOM_W;x++){rooms[2].walls[x][0]=T.WALL;rooms[2].walls[x][ROOM_H-1]=T.WALL;}
for(let y=0;y<ROOM_H;y++){rooms[2].walls[0][y]=T.WALL;rooms[2].walls[ROOM_W-1][y]=T.WALL;}
// Crystal formations (walls)
[[3,4],[3,5],[4,6],[7,3],[8,3],[9,3],[14,5],[15,5],[16,5],[14,6],
[5,10],[6,10],[7,10],[5,11],[12,8],[13,8],[14,8]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[2].walls[x][y]=T.WALL;});
// Doors: left to room 1, down to room 4
rooms[2].walls[0][7]=T.DOOR;
rooms[2].walls[10][ROOM_H-1]=T.DOOR;
rooms[2].exits = { left:1, down:4 };
// Enemies (harder)
rooms[2].enemies.push({x:5,y:7,type:'crystalGolem',hp:20,maxHp:20,dmg:4,speed:0.6,xp:35,gold:15});
rooms[2].enemies.push({x:12,y:4,type:'wolf',hp:18,maxHp:18,dmg:4,speed:1.4,xp:30,gold:12});
rooms[2].enemies.push({x:9,y:12,type:'crystalGolem',hp:25,maxHp:25,dmg:5,speed:0.7,xp:40,gold:18});
// Ranged weapon discovery!
rooms[2].items.push({x:8,y:6,type:'rangedWeapon',collected:false});
rooms[2].items.push({x:3,y:9,type:'gold',amount:30,collected:false});
rooms[2].items.push({x:15,y:11,type:'manaPotion',collected:false}); // Accessory 2
// Room 3: Merchant's Keep (shop)
rooms[3].walls = [];
for(let x=0;x<ROOM_W;x++){rooms[3].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[3].walls[x][y]=T.FLOOR;}
for(let x=0;x<ROOM_W;x++){rooms[3].walls[x][0]=T.WALL;rooms[3].walls[x][ROOM_H-1]=T.WALL;}
for(let y=0;y<ROOM_H;y++){rooms[3].walls[0][y]=T.WALL;rooms[3].walls[ROOM_W-1][y]=T.WALL;}
// Shop counter
[[8,4],[9,4],[10,4],[11,4]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[3].walls[x][y]=T.SHOP;});
rooms[3].walls[10][ROOM_H-1]=T.DOOR; // up to room 1
rooms[3].exits = { up:1 };
// NPC merchant at shop counter
rooms[3].npcs.push({x:9.5,y:5,type:'merchant'});
// Save point in keep
rooms[3].items.push({x:4,y:8,type:'savepoint',collected:false});
rooms[3].items.push({x:15,y:6,type:'gold',amount:25,collected:false});
// Room 4: Shadow Fortress (final)
rooms[4].walls = [];
for(let x=0;x<ROOM_W;x++){rooms[4].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[4].walls[x][y]=T.FLOOR;}
for(let x=0;x<ROOM_W;x++){rooms[4].walls[x][0]=T.WALL;rooms[4].walls[x][ROOM_H-1]=T.WALL;}
for(let y=0;y<ROOM_H;y++){rooms[4].walls[0][y]=T.WALL;rooms[4].walls[ROOM_W-1][y]=T.WALL;}
// Fortress walls
[[3,3],[4,3],[5,3],[6,3],[3,4],[3,5],[14,3],[15,3],[16,3],[17,3],[17,4],[17,5],
[8,8],[9,8],[10,8],[11,8],[12,8],[8,9],[12,9]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[4].walls[x][y]=T.WALL;});
rooms[4].walls[10][0]=T.DOOR; // up from room 2
rooms[4].exits = { up:2 };
// Enemies (hardest)
rooms[4].enemies.push({x:5,y:6,type:'shadowKnight',hp:30,maxHp:30,dmg:6,speed:1.0,xp:50,gold:25});
rooms[4].enemies.push({x:15,y:7,type:'shadowKnight',hp:30,maxHp:30,dmg:6,speed:1.0,xp:50,gold:25});
rooms[4].enemies.push({x:8,y:11,type:'crystalGolem',hp:35,maxHp:35,dmg:7,speed:0.8,xp:55,gold:30});
// Boss
rooms[4].bossEnemy = {x:9.5,y:12,type:'shadowLord',hp:60,maxHp:60,dmg:8,speed:0.9,xp:200,gold:100,defeated:false};
// Items
rooms[4].items.push({x:3,y:12,type:'healthPotion',collected:false});
rooms[4].items.push({x:16,y:12,type:'gold',amount:50,collected:false});
}
// ==================== SHOP DATA ====================
const shopItems = [
{ id:'healthPotion', name:'Health Potion', desc:'Restores 4 HP', cost:20, icon:'🧪' },
{ id:'manaPotion', name:'Mana Crystal', desc:'+1 Max Mana (ranged)', cost:35, icon:'💎' },
{ id:'ironArmor', name:'Iron Armor', desc:'+2 Max HP', cost:60, icon:'🛡️' },
{ id:'speedBoots', name:'Swift Boots', desc:'+15% Move Speed', cost:45, icon:'👢' },
{ id:'powerRing', name:'Power Ring', desc:'+2 Attack Damage', cost:50, icon:'💍' },
];
// ==================== GAME STATE ====================
let game = null;
function createGameState() {
return {
running: false,
paused: false,
player: {
x: 3 * TILE + TILE/2, y: 7 * TILE + TILE/2,
room: 0,
hp: 6, maxHp: 6, // starts at 3 hearts = 6 HP (each heart = 2)
xp: 0, level: 1, nextLevelXp: 50,
gold: 10,
weapon: { name:'Iron Sword', damage:4, speed:0.4, range:36, color:'#aaa' },
weaponLevel: 0, // 0=iron, 1=steel, 2=fury blade
hasRangedWeapon: false,
mana: 5, maxMana: 5,
accessories: [],
speed: 2.5,
attackCooldown: 0,
invincibleTimer: 0,
facing: {x:1,y:0},
walkFrame: 0,
damageFlash: 0,
},
projectiles: [],
enemies: [],
particles: [],
notifications: [],
dialogText: '',
dialogTimer: 0,
shopOpen: false,
inventoryOpen: false,
mapOpen: false,
keysDown: {},
lastWalkSound: 0,
gameTime: 0,
bossDefeated: false,
weaponUpgradesFound: 0,
};
}
// ==================== PARTICLE SYSTEM ====================
function spawnParticles(x, y, color, count=8, speed=3, life=30) {
for(let i=0;i<count;i++){
const angle = Math.random()*Math.PI*2;
const spd = speed*(0.5+Math.random());
game.particles.push({
x, y, vx:Math.cos(angle)*spd, vy:Math.sin(angle)*spd,
life, maxLife:life, color, size: 2+Math.random()*3
});
}
}
function spawnDamageNumber(x, y, amount) {
game.particles.push({
x, y, vx:(Math.random()-0.5)*1, vy:-2,
life:40, maxLife:40, color:'#ff4', size:0, text:'-'+amount, isText:true
});
}
function spawnGoldNumber(x, y, amount) {
game.particles.push({
x, y, vx:(Math.random()-0.5)*1, vy:-1.5,
life:45, maxLife:45, color:'#ffd700', size:0, text:'+'+amount+'🪙', isText:true
});
}
function spawnXpNumber(x, y, amount) {
game.particles.push({
x, y, vx:(Math.random()-0.5)*1, vy:-1.2,
life:40, maxLife:40, color:'#6f6', size:0, text:'+'+amount+' XP', isText:true
});
}
// ==================== INPUT ====================
document.addEventListener('keydown', e => {
if (!game || !game.running) return;
game.keysDown[e.key.toLowerCase()] = true;
if (e.key === 'i' || e.key === 'I') toggleInventory();
if (e.key === 'm' || e.key === 'M') toggleMap();
if (e.key === 'Escape') { closeAllPanels(); }
if (e.key >= '1' && e.key <= '3') useItemSlot(parseInt(e.key)-1);
});
document.addEventListener('keyup', e => {
if (!game) return;
game.keysDown[e.key.toLowerCase()] = false;
});
canvas.addEventListener('click', e => {
if (!game || !game.running || game.shopOpen || game.inventoryOpen) return;
// Ranged attack on click (if has ranged weapon and shift held or just click)
if (game.player.hasRangedWeapon && game.keysDown['shift']) {
fireProjectile(e.clientX, e.clientY);
}
});
// ==================== UI FUNCTIONS ====================
function updateHUD() {
const p = game.player;
const hearts = document.querySelectorAll('.heart');
const totalHearts = Math.ceil(p.maxHp / 2);
// Adjust heart count display
while(hearts.length < 5) {
const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
svg.setAttribute('class','heart');
svg.setAttribute('viewBox','0 0 24 24');
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('fill','#e33');
path.setAttribute('d','M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z');
svg.appendChild(path);
document.getElementById('hud').insertBefore(svg, document.getElementById('gold-display'));
}
const allHearts = document.querySelectorAll('.heart');
for(let i=0;i<allHearts.length;i++){
if(i < totalHearts) {
allHearts[i].style.display = '';
const hpPerHeart = 2;
const heartHp = Math.min(hpPerHeart, p.hp - i*hpPerHeart);
allHearts[i].classList.toggle('empty', heartHp <= 0);
} else {
allHearts[i].style.display = 'none';
}
}
document.getElementById('gold-display').textContent = '🪙 ' + p.gold;
document.getElementById('weapon-name').textContent = p.weapon.name;
document.getElementById('level-display').textContent = 'Lv ' + p.level;
const xpPct = (p.xp / p.nextLevelXp) * 100;
document.getElementById('xp-bar').style.width = xpPct + '%';
}
function showNotification(text, duration=2000) {
game.notifications.push({text, timer:duration});
const el = document.getElementById('notification');
el.textContent = text;
el.style.opacity = '1';
setTimeout(()=>{el.style.opacity='0';}, duration);
}
function showDialog(text, duration=3000) {
game.dialogText = text;
game.dialogTimer = duration;
const el = document.getElementById('dialog-box');
el.textContent = text;
el.style.display = 'block';
}
function toggleInventory() {
if (game.shopOpen) return;
game.inventoryOpen = !game.inventoryOpen;
game.mapOpen = false;
document.getElementById('inv-panel').style.display = game.inventoryOpen ? 'block' : 'none';
if(game.inventoryOpen) updateInvPanel();
}
function toggleMap() {
game.mapOpen = !game.mapOpen;
game.inventoryOpen = false;
document.getElementById('inv-panel').style.display = 'none';
}
function closeAllPanels() {
game.shopOpen = false;
game.inventoryOpen = false;
game.mapOpen = false;
document.getElementById('shop-panel').style.display = 'none';
document.getElementById('inv-panel').style.display = 'none';
}
function updateInvPanel() {
const p = game.player;
let html = `<div class="inv-item"><span>⚔️ ${p.weapon.name}</span><span style="color:#aaa">DMG:${p.weapon.damage}</span></div>`;
if(p.hasRangedWeapon) html += `<div class="inv-item"><span>🏹 Crystal Bow</span><span style="color:#88f">Mana: ${p.mana}/${p.maxMana}</span></div>`;
p.accessories.forEach((acc, i) => {
const icons = {'ironArmor':'🛡️','speedBoots':'👢','powerRing':'💍','shield':'🔰'};
html += `<div class="inv-item"><span>${icons[acc.id]||'✦'} ${acc.name}</span></div>`;
});
// Consumables
const consumables = p.accessories.filter(a => a.consumable);
if(consumables.length > 0) {
html += '<hr style="border-color:#333;margin:8px 0;">';
consumables.forEach((c,i) => {
html += `<div class="inv-item"><span>${c.icon||'🧪'} ${c.name}</span><span class="use-btn" onclick="useItemSlot(${i})">Use</span></div>`;
});
}
document.getElementById('inv-items').innerHTML = html;
}
function useItemSlot(slot) {
const p = game.player;
const consumables = p.accessories.filter(a => a.consumable);
if(slot < consumables.length) {
const item = consumables[slot];
if(item.id === 'healthPotion') {
p.hp = Math.min(p.maxHp, p.hp + 4);
playSound('heal');
spawnParticles(p.x, p.y, '#f66', 12, 2, 25);
showNotification('+4 HP Restored!');
} else if(item.id === 'manaPotion') {
p.mana = Math.min(p.maxMana, p.mana + 3);
playSound('heal');
spawnParticles(p.x, p.y, '#68f', 12, 2, 25);
showNotification('+3 Mana Restored!');
}
// Remove consumable
const idx = p.accessories.indexOf(item);
if(idx >= 0) p.accessories.splice(idx, 1);
updateInvPanel();
updateHUD();
}
}
function openShop() {
game.shopOpen = true;
playSound('shop');
const panel = document.getElementById('shop-panel');
let html = '';
shopItems.forEach(item => {
const owned = game.player.accessories.some(a => a.id === item.id && !a.consumable);
html += `<div class="shop-item" onclick="buyItem('${item.id}')" style="${owned?'opacity:0.4;pointer-events:none':''}">
<span class="name">${item.icon} ${item.name}</span>
<span style="font-size:11px;color:#999;">${item.desc}</span>
<span class="cost">🪙 ${item.cost}</span>
</div>`;
});
document.getElementById('shop-items').innerHTML = html;
panel.style.display = 'block';
}
function buyItem(id) {
const item = shopItems.find(i => i.id === id);
if(!item || game.player.gold < item.cost) { showNotification("Not enough gold!"); return; }
game.player.gold -= item.cost;
playSound('pickup');
const acc = { id: item.id, name: item.name, icon: item.icon, consumable: item.consumable || false };
if(item.id === 'ironArmor') {
game.player.maxHp += 2;
game.player.hp += 2;
showNotification("Iron Armor equipped! +2 Max HP");
} else if(item.id === 'speedBoots') {
game.player.speed *= 1.15;
showNotification("Swift Boots equipped! Speed increased!");
} else if(item.id === 'powerRing') {
game.player.weapon.damage += 2;
showNotification("Power Ring equipped! +2 Damage");
} else {
acc.consumable = true;
showNotification(`Purchased ${item.name}!`);
}
game.player.accessories.push(acc);
spawnParticles(game.player.x, game.player.y, '#ffd700', 15, 3, 20);
updateHUD();
openShop(); // Refresh shop display
}
// ==================== GAME LOGIC ====================
function startGame() {
initAudio();
document.getElementById('title-screen').style.display = 'none';
game = createGameState();
buildMaps();
// Load saved game if exists
loadGame();
game.running = true;
spawnRoomEnemies(game.player.room);
requestAnimationFrame(gameLoop);
}
function spawnRoomEnemies(roomIdx) {
const room = rooms[roomIdx];
game.enemies = [];
room.enemies.forEach(e => {
if(!e.dead) {
game.enemies.push({...e, x: e.x*TILE+TILE/2, y: e.y*TILE+TILE/2,
attackCooldown:0, hitFlash:0, state:'patrol', patrolAngle:Math.random()*Math.PI*2});
}
});
if(room.bossEnemy && !room.bossEnemy.defeated) {
game.enemies.push({...room.bossEnemy, x: room.bossEnemy.x*TILE+TILE/2, y: room.bossEnemy.y*TILE+TILE/2,
attackCooldown:0, hitFlash:0, state:'patrol', patrolAngle:Math.random()*Math.PI*2});
}
}
function gameLoop(timestamp) {
if(!game || !game.running) return;
const dt = 1/60;
game.gameTime += dt;
update(dt);
render();
requestAnimationFrame(gameLoop);
}
function update(dt) {
if(game.shopOpen || game.inventoryOpen) return;
const p = game.player;
// Movement
let dx=0, dy=0;
if(game.keysDown['w']||game.keysDown['arrowup']) dy=-1;
if(game.keysDown['s']||game.keysDown['arrowdown']) dy=1;
if(game.keysDown['a']||game.keysDown['arrowleft']) dx=-1;
if(game.keysDown['d']||game.keysDown['arrowright']) dx=1;
if(dx!==0 || dy!==0) {
const len = Math.sqrt(dx*dx+dy*dy);
dx/=len; dy/=len;
p.facing.x = dx; p.facing.y = dy;
const newX = p.x + dx * p.speed;
const newY = p.y + dy * p.speed;
// Collision check
if(!isWall(newX, p.y)) p.x = newX;
if(!isWall(p.x, newY)) p.y = newY;
p.walkFrame += 0.15;
// Walk sound
if(game.gameTime - game.lastWalkSound > 0.3) {
playSound('walk', 0.08);
game.lastWalkSound = game.gameTime;
}
}
// Room transitions
checkRoomTransition();
// Attack
p.attackCooldown -= dt;
if(game.keysDown[' '] && p.attackCooldown <= 0) {
performAttack();
p.attackCooldown = p.weapon.speed;
}
// Invincibility timer
if(p.invincibleTimer > 0) p.invincibleTimer -= dt;
if(p.damageFlash > 0) p.damageFlash -= dt;
// Interact (E key)
if(game.keysDown['e']) {
checkInteractions();
game.keysDown['e'] = false;
}
// Update enemies
updateEnemies(dt);
// Update projectiles
updateProjectiles(dt);
// Update particles
game.particles = game.particles.filter(pt => {
pt.x += pt.vx;
pt.y += pt.vy;
pt.life--;
return pt.life > 0;
});
// Dialog timer
if(game.dialogTimer > 0) {
game.dialogTimer -= dt*1000;
if(game.dialogTimer <= 0) document.getElementById('dialog-box').style.display = 'none';
}
updateHUD();
}
function isWall(px, py) {
const room = rooms[game.player.room];
const tx = Math.floor(px / TILE);
const ty = Math.floor(py / TILE);
if(tx<0||tx>=ROOM_W||ty<0||ty>=ROOM_H) return true;
const tile = room.walls[tx][ty];
return tile === T.WALL || tile === T.WATER;
}
function checkRoomTransition() {
const p = game.player;
const room = rooms[p.room];
// Check if player is at a door position
const tx = Math.floor(p.x / TILE);
const ty = Math.floor(p.y / TILE);
for(const [dir, targetRoom] of Object.entries(room.exits)) {
let edgeX, edgeY;
switch(dir) {
case 'right': edgeX=ROOM_W-1; edgeY=ty; break;
case 'left': edgeX=0; edgeY=ty; break;
case 'down': edgeX=tx; edgeY=ROOM_H-1; break;
case 'up': edgeX=tx; edgeY=0; break;
}
if(tx===edgeX && ty===edgeY) {
// Transition!
p.room = targetRoom;
// Set entry position
switch(dir) {
case 'right': p.x=TILE*1.5; p.y=ty*TILE+TILE/2; break;
case 'left': p.x=(ROOM_W-1.5)*TILE; p.y=ty*TILE+TILE/2; break;
case 'down': p.x=tx*TILE+TILE/2; p.y=TILE*1.5; break;
case 'up': p.x=tx*TILE+TILE/2; p.y=(ROOM_H-1.5)*TILE; break;
}
spawnRoomEnemies(targetRoom);
showNotification(`Entered ${rooms[targetRoom].name}`);
playSound('pickup');
}
}
}
function performAttack() {
const p = game.player;
playSound('attack');
// Melee attack hitbox
const ax = p.x + p.facing.x * 20;
const ay = p.y + p.facing.y * 20;
spawnParticles(ax, ay, p.weapon.color, 5, 2, 15);
game.enemies.forEach(e => {
if(e.dead) return;
const dist = Math.sqrt((e.x-ax)**2 + (e.y-ay)**2);
if(dist < p.weapon.range + ENEMY_SIZE/2) {
damageEnemy(e, p.weapon.damage);
}
});
}
function fireProjectile(targetX, targetY) {
const p = game.player;
if(p.mana <= 0) { showNotification("No mana!"); return; }
p.mana--;
playSound('ranged');
const dx = targetX - (p.x + getCameraOffset().x);
const dy = targetY - (p.y + getCameraOffset().y);
const len = Math.sqrt(dx*dx+dy*dy) || 1;
game.projectiles.push({
x: p.x, y: p.y,
vx: (dx/len)*6, vy: (dy/len)*6,
damage: 3 + p.weaponLevel * 2,
life: 80, color: '#4af'
});
spawnParticles(p.x, p.y, '#4af', 5, 2, 10);
}
function updateProjectiles(dt) {
game.projectiles = game.projectiles.filter(proj => {
proj.x += proj.vx;
proj.y += proj.vy;
proj.life--;
// Hit enemies
for(const e of game.enemies) {
if(e.dead) continue;
const dist = Math.sqrt((e.x-proj.x)**2 + (e.y-proj.y)**2);
if(dist < ENEMY_SIZE/2 + 6) {
damageEnemy(e, proj.damage);
spawnParticles(proj.x, proj.y, '#4af', 8, 3, 15);
return false;
}
}
// Hit wall
if(isWall(proj.x, proj.y)) {
spawnParticles(proj.x, proj.y, '#666', 4, 2, 10);
return false;
}
return proj.life > 0;
});
}
function damageEnemy(e, dmg) {
e.hp -= dmg;
e.hitFlash = 0.2;
spawnDamageNumber(e.x, e.y - 15, dmg);
spawnParticles(e.x, e.y, '#f84', 6, 3, 15);
playSound('hit');
if(e.hp <= 0) {
e.dead = true;
const p = game.player;
p.gold += e.gold || 0;
p.xp += e.xp || 0;
spawnGoldNumber(e.x, e.y - 5, e.gold);
spawnXpNumber(e.x, e.y + 10, e.xp);
spawnParticles(e.x, e.y, '#f44', 20, 4, 30);
// Check level up
checkLevelUp();
// Boss defeated?
if(e.type === 'shadowLord') {
game.bossDefeated = true;
rooms[4].bossEnemy.defeated = true;
setTimeout(()=>{
document.getElementById('victory-screen').style.display='flex';
playSound('levelup');
}, 1500);
}
// Weapon upgrade drops
if(e.type === 'wolf' && game.player.weaponLevel < 1) {
upgradeWeapon(1, 'Steel Sword', 7, 0.35, '#8cf');
} else if((e.type === 'shadowKnight') && game.player.weaponLevel < 2) {
upgradeWeapon(2, 'Fury Blade', 12, 0.28, '#f4a');
}
}
}
function checkLevelUp() {
const p = game.player;
while(p.xp >= p.nextLevelXp) {
p.xp -= p.nextLevelXp;
p.level++;
p.nextLevelXp = Math.floor(p.nextLevelXp * 1.5);
// Level up bonuses
if(p.level === 2) {
p.maxHp += 2; p.hp += 2;
showNotification("Level Up! Max HP increased to " + p.maxHp);
} else if(p.level >= 3 && p.maxHp < 10) {
p.maxHp = Math.min(10, p.maxHp + 2);
p.hp += 2;
showNotification("Level Up! Max HP increased to " + p.maxHp);
} else {
p.weapon.damage += 1;
showNotification(`Level ${p.level}! Damage increased!`);
}
playSound('levelup');
spawnParticles(p.x, p.y, '#ff0', 25, 4, 35);
}
}
function upgradeWeapon(level, name, damage, speed, color) {
const p = game.player;
if(p.weaponLevel >= level) return;
p.weaponLevel = level;
p.weapon.name = name;
p.weapon.damage = damage;
p.weapon.speed = speed;
p.weapon.color = color;
showNotification(`⚔️ Weapon upgraded to ${name}!`);
playSound('levelup');
spawnParticles(p.x, p.y, '#ffd700', 30, 5, 40);
updateHUD();
}
function updateEnemies(dt) {
const p = game.player;
game.enemies.forEach(e => {
if(e.dead) return;
e.hitFlash -= dt;
e.attackCooldown -= dt;
const distToPlayer = Math.sqrt((e.x-p.x)**2 + (e.y-p.y)**2);
// AI: Chase player when close
if(distToPlayer < 180) {
e.state = 'chase';
const dx = p.x - e.x;
const dy = p.y - e.y;
const len = Math.sqrt(dx*dx+dy*dy) || 1;
const nx = e.x + (dx/len)*e.speed;
const ny = e.y + (dy/len)*e.speed;
if(!isWall(nx, e.y)) e.x = nx;
if(!isWall(e.x, ny)) e.y = ny;
// Attack player
if(distToPlayer < 28 && e.attackCooldown <= 0) {
damagePlayer(e.dmg);
e.attackCooldown = 1.0;
}
} else {
// Patrol
e.state = 'patrol';
e.patrolAngle += (Math.random()-0.5)*0.1;
const nx = e.x + Math.cos(e.patrolAngle)*e.speed*0.3;
const ny = e.y + Math.sin(e.patrolAngle)*e.speed*0.3;
if(!isWall(nx, e.y)) e.x = nx; else e.patrolAngle += Math.PI;
if(!isWall(e.x, ny)) e.y = ny; else e.patrolAngle += Math.PI;
}
});
}
function damagePlayer(dmg) {
const p = game.player;
if(p.invincibleTimer > 0) return;
// Shield accessory reduces damage
const shieldBonus = p.accessories.some(a => a.id === 'shield') ? -1 : 0;
dmg = Math.max(1, dmg + shieldBonus);
p.hp -= dmg;
p.invincibleTimer = 0.8;
p.damageFlash = 0.3;
playSound('hit');
spawnParticles(p.x, p.y, '#f22', 10, 3, 20);
spawnDamageNumber(p.x, p.y - 20, dmg);
if(p.hp <= 0) {
p.hp = 0;
playSound('death');
document.getElementById('game-over-screen').style.display = 'flex';
game.running = false;
}
}
function checkInteractions() {
const p = game.player;
const room = rooms[p.room];
// Check items at player position
const tx = Math.floor(p.x / TILE);
const ty = Math.floor(p.y / TILE);
room.items.forEach(item => {
if(item.collected) return;
if(Math.abs(tx - item.x) <= 1 && Math.abs(ty - item.y) <= 1) {
collectItem(item);
}
});
// Check NPCs
room.npcs.forEach(npc => {
const dist = Math.sqrt((p.x - npc.x*TILE)**2 + (p.y - npc.y*TILE)**2);
if(dist < 48) {
if(npc.type === 'merchant') openShop();
}
});
}
function collectItem(item) {
item.collected = true;
const p = game.player;
switch(item.type) {
case 'gold':
p.gold += item.amount;
spawnGoldNumber(p.x, p.y - 10, item.amount);
showNotification(`+${item.amount} Gold`);
break;
case 'healthPotion':
if(p.accessories.filter(a=>a.consumable).length < 5) {
p.accessories.push({id:'healthPotion',name:'Health Potion',icon:'🧪',consumable:true});
showNotification("Found Health Potion!");
} else {
p.hp = Math.min(p.maxHp, p.hp + 4);
showNotification("+4 HP");
}
break;
case 'manaPotion':
if(p.accessories.filter(a=>a.consumable).length < 5) {
p.accessories.push({id:'manaPotion',name:'Mana Crystal',icon:'💎',consumable:true});
showNotification("Found Mana Crystal!");
} else {
p.mana = Math.min(p.maxMana, p.mana + 3);
showNotification("+3 Mana");
}
break;
case 'shield':
if(!p.accessories.some(a=>a.id==='shield')) {
p.accessories.push({id:'shield',name:'Wooden Shield',icon:'🔰',consumable:false});
showNotification("Found Wooden Shield! (-1 damage taken)");
}
break;
case 'rangedWeapon':
if(!p.hasRangedWeapon) {
p.hasRangedWeapon = true;
p.maxMana = 5;
p.mana = 5;
showNotification("🏹 Discovered Crystal Bow! Hold SHIFT + Click to fire!");
spawnParticles(p.x, p.y, '#4af', 30, 5, 40);
}
break;
case 'savepoint':
saveGame();
showDialog("💾 Progress Saved!", 2000);
playSound('save');
spawnParticles(p.x, p.y, '#6f6', 20, 3, 35);
return; // Don't play pickup sound for savepoints
}
playSound('pickup');
spawnParticles(p.x, p.y, '#ffd700', 10, 3, 20);
}
// ==================== SAVE SYSTEM ====================
function saveGame() {
const data = {
player: game.player,
rooms: rooms.map(r => ({
items: r.items.map(i => ({...i})),
enemies: r.enemies.map(e => ({type:e.type, dead:e.dead||false})),
bossEnemy: r.bossEnemy ? {...r.bossEnemy} : null
})),
gameTime: game.gameTime,
bossDefeated: game.bossDefeated
};
localStorage.setItem('realmOfEchoes', JSON.stringify(data));
}
function loadGame() {
try {
const raw = localStorage.getItem('realmOfEchoes');
if(!raw) return;
const data = JSON.parse(raw);
Object.assign(game.player, data.player);
// Restore room states
data.rooms.forEach((rd, i) => {
if(rooms[i]) {
rooms[i].items = rd.items || rooms[i].items;
if(rd.enemies && rooms[i].enemies) {
rd.enemies.forEach((re, j) => {
if(re.dead && rooms[i].enemies[j]) rooms[i].enemies[j].dead = true;
});
}
if(rd.bossEnemy) rooms[i].bossEnemy = rd.bossEnemy;
}
});
game.gameTime = data.gameTime || 0;
game.bossDefeated = data.bossDefeated || false;
} catch(e) { /* ignore save errors */ }
}
// ==================== RENDERING ====================
function getCameraOffset() {
const p = game.player;
return {
x: canvas.width/2 - p.x,
y: canvas.height/2 - p.y
};
}
function render() {
ctx.fillStyle = '#0a0a0f';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const cam = getCameraOffset();
const room = rooms[game.player.room];
// Draw floor
for(let x=0;x<ROOM_W;x++) {
for(let y=0;y<ROOM_H;y++) {
const tile = room.walls[x][y];
const sx = x*TILE + cam.x;
const sy = y*TILE + cam.y;
if(tile === T.WALL) {
ctx.fillStyle = room.bg;
ctx.fillRect(sx, sy, TILE, TILE);
// Wall detail
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(sx+2, sy+2, TILE-4, TILE-4);
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.strokeRect(sx, sy, TILE, TILE);
} else if(tile === T.DOOR) {
ctx.fillStyle = room.floor;
ctx.fillRect(sx, sy, TILE, TILE);
// Door frame
ctx.fillStyle = '#654';
ctx.fillRect(sx+4, sy+2, TILE-8, TILE-4);
ctx.fillStyle = '#a86';
ctx.fillRect(sx+6, sy+4, TILE-12, TILE-8);
} else if(tile === T.SHOP) {
ctx.fillStyle = room.floor;
ctx.fillRect(sx, sy, TILE, TILE);
ctx.fillStyle = '#5a3a10';
ctx.fillRect(sx+2, sy+4, TILE-4, TILE-6);
} else {
ctx.fillStyle = room.floor;
ctx.fillRect(sx, sy, TILE, TILE);
// Floor detail
if((x+y)%3===0) {
ctx.fillStyle = 'rgba(255,255,255,0.03)';
ctx.fillRect(sx+8, sy+8, 4, 4);
}
}
}
}
// Draw items on ground
room.items.forEach(item => {
if(item.collected) return;
const sx = item.x*TILE + TILE/2 + cam.x;
const sy = item.y*TILE + TILE/2 + cam.y;
const bob = Math.sin(game.gameTime*3)*3;
switch(item.type) {
case 'gold':
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(sx, sy+bob, 6, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ff8';
ctx.font='10px sans-serif';
ctx.textAlign='center';
ctx.fillText('$',sx,sy+bob+4);
break;
case 'healthPotion':
ctx.fillStyle='#f44';
ctx.beginPath();ctx.arc(sx,sy+bob,7,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';
ctx.fillRect(sx-1.5,sy-3+bob,3,6);
ctx.fillRect(sx-3,sy-1+bob,6,3);
break;
case 'manaPotion':
ctx.fillStyle='#48f';
ctx.beginPath();ctx.arc(sx,sy+bob,7,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#aef';
ctx.fillRect(sx-1.5,sy-3+bob,3,6);
ctx.fillRect(sx-3,sy-1+bob,6,3);
break;
case 'shield':
ctx.fillStyle='#8B4513';
ctx.beginPath();
ctx.moveTo(sx-7,sy-8+bob);ctx.lineTo(sx+7,sy-8+bob);
ctx.lineTo(sx+7,sy+2+bob);ctx.lineTo(sx,sy+9+bob);
ctx.lineTo(sx-7,sy+2+bob);ctx.closePath();ctx.fill();
break;
case 'rangedWeapon':
// Glowing crystal bow
const glow = 0.5 + Math.sin(game.gameTime*4)*0.3;
ctx.fillStyle=`rgba(68,170,255,${glow})`;
ctx.beginPath();ctx.arc(sx,sy+bob,10,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='#4af';ctx.lineWidth=2;
ctx.beginPath();ctx.arc(sx,sy+bob,6,0.5,Math.PI-0.5);ctx.stroke();
break;
case 'savepoint':
// Glowing crystal
const sg = 0.4+Math.sin(game.gameTime*2)*0.3;
ctx.fillStyle=`rgba(100,255,100,${sg})`;
ctx.beginPath();ctx.arc(sx,sy+bob,8,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#6f6';
ctx.font='bold 14px sans-serif';ctx.textAlign='center';
ctx.fillText('💾',sx,sy+5+bob);
break;
}
});
// Draw NPCs
room.npcs.forEach(npc => {
const sx = npc.x*TILE + cam.x;
const sy = npc.y*TILE + cam.y;
if(npc.type === 'merchant') {
// Merchant body
ctx.fillStyle='#8B4513';
ctx.fillRect(sx-8,sy-2,16,18);
// Head
ctx.fillStyle='#daa070';
ctx.beginPath();ctx.arc(sx,sy-8,8,0,Math.PI*2);ctx.fill();
// Hat
ctx.fillStyle='#532';
ctx.fillRect(sx-9,sy-16,18,6);
ctx.fillRect(sx-6,sy-20,12,6);
// Sign
ctx.fillStyle='#ffd700';ctx.font='bold 10px sans-serif';ctx.textAlign='center';
ctx.fillText('SHOP',sx,sy+24);
}
});
// Draw enemies
game.enemies.forEach(e => {
if(e.dead) return;
const sx = e.x + cam.x;
const sy = e.y + cam.y;
let bodyColor, eyeColor;
switch(e.type) {
case 'slime': bodyColor='#4a2'; eyeColor='#0f0'; break;
case 'wolf': bodyColor='#654'; eyeColor='#f80'; break;
case 'crystalGolem': bodyColor='#369'; eyeColor='#aef'; break;
case 'shadowKnight': bodyColor='#212'; eyeColor='#f0f'; break;
case 'shadowLord': bodyColor='#100'; eyeColor='#f44'; break;
}
// Hit flash
if(e.hitFlash > 0) bodyColor = '#fff';
const size = e.type==='shadowLord' ? ENEMY_SIZE*1.5 : ENEMY_SIZE;
// Shadow
ctx.fillStyle='rgba(0,0,0,0.3)';
ctx.beginPath();ctx.ellipse(sx,sy+size/2+2,size/2,4,0,0,Math.PI*2);ctx.fill();
// Body
ctx.fillStyle=bodyColor;
if(e.type==='slime') {
const squish = 1 + Math.sin(game.gameTime*3)*0.1;
ctx.beginPath();
ctx.ellipse(sx,sy,size/2,size/2*squish,0,0,Math.PI*2);
ctx.fill();
} else if(e.type==='shadowLord') {
// Boss: larger, more menacing
ctx.fillRect(sx-size/2,sy-size/2,size,size);
// Cape
ctx.fillStyle='#300';
ctx.beginPath();
ctx.moveTo(sx-size/2-4,sy+size/2);
ctx.lineTo(sx,sy+size/2+15);
ctx.lineTo(sx+size/2+4,sy+size/2);
ctx.fill();
} else {
ctx.fillRect(sx-size/2,sy-size/2,size,size);
}
// Eyes
ctx.fillStyle=eyeColor;
const eyeOff = size*0.15;
ctx.fillRect(sx-eyeOff-2,sy-3,4,4);
ctx.fillRect(sx+eyeOff-2,sy-3,4,4);
// HP bar
if(e.hp < e.maxHp) {
const bw = size + 10;
ctx.fillStyle='rgba(0,0,0,0.6)';
ctx.fillRect(sx-bw/2,sy-size/2-8,bw,4);
ctx.fillStyle=e.type==='shadowLord'?'#f44':'#c33';
ctx.fillRect(sx-bw/2,sy-size/2-8,bw*(e.hp/e.maxHp),4);
}
});
// Draw player
drawPlayer(cam.x, cam.y);
// Draw projectiles
game.projectiles.forEach(proj => {
const sx = proj.x + cam.x;
const sy = proj.y + cam.y;
ctx.fillStyle=proj.color;
ctx.beginPath();ctx.arc(sx,sy,4,0,Math.PI*2);ctx.fill();
// Trail
ctx.fillStyle='rgba(68,170,255,0.3)';
ctx.beginPath();ctx.arc(sx-proj.vx,sy-proj.vy,3,0,Math.PI*2);ctx.fill();
});
// Draw particles
game.particles.forEach(pt => {
const sx = pt.x + cam.x;
const sy = pt.y + cam.y;
const alpha = pt.life / pt.maxLife;
if(pt.isText) {
ctx.globalAlpha=alpha;
ctx.fillStyle=pt.color;
ctx.font='bold 13px sans-serif';
ctx.textAlign='center';
ctx.fillText(pt.text,sx,sy);
ctx.globalAlpha=1;
} else {
ctx.globalAlpha=alpha;
ctx.fillStyle=pt.color;
ctx.beginPath();ctx.arc(sx,sy,pt.size*alpha,0,Math.PI*2);ctx.fill();
ctx.globalAlpha=1;
}
});
// Room name display
ctx.fillStyle='rgba(255,255,255,0.15)';
ctx.font='bold 16px sans-serif';
ctx.textAlign='center';
ctx.fillText(room.name, canvas.width/2, canvas.height-30);
// Minimap
drawMinimap();
}
function drawPlayer(cx, cy) {
const p = game.player;
const sx = p.x + cx;
const sy = p.y + cy;
// Damage flash
if(p.damageFlash > 0 && Math.floor(game.gameTime*20)%2===0) return;
// Invincibility blink
if(p.invincibleTimer > 0 && Math.floor(game.gameTime*15)%2===0) {
ctx.globalAlpha = 0.4;
}
const bobY = Math.sin(p.walkFrame)*2;
// Shadow
ctx.fillStyle='rgba(0,0,0,0.3)';
ctx.beginPath();ctx.ellipse(sx,sy+14,10,4,0,0,Math.PI*2);ctx.fill();
// Body color based on armor
let bodyColor = '#4a8';
if(p.accessories.some(a=>a.id==='ironArmor')) bodyColor = '#667';
// Legs (animated)
const legSwing = Math.sin(p.walkFrame*2)*3;
ctx.fillStyle='#356';
ctx.fillRect(sx-4,sy+4+bobY,4,10);
ctx.fillRect(sx+1,sy+4+bobY,4,10);
// Body
ctx.fillStyle=bodyColor;
ctx.fillRect(sx-7,sy-6+bobY,14,14);
// Head
ctx.fillStyle='#daa070';
ctx.beginPath();ctx.arc(sx,sy-10+bobY,7,0,Math.PI*2);ctx.fill();
// Eyes (facing direction)
const eyeX = p.facing.x * 2;
ctx.fillStyle='#333';
ctx.fillRect(sx+eyeX-3,sy-11+bobY,2,2);
ctx.fillRect(sx+eyeX+1,sy-11+bobY,2,2);
// Weapon in hand
const weaponAngle = Math.atan2(p.facing.y, p.facing.x);
const attackSwing = (p.attackCooldown > 0) ? Math.sin(p.attackCooldown/p.weapon.speed * Math.PI)*30 : 0;
ctx.save();
ctx.translate(sx + p.facing.x*8, sy + p.facing.y*8 + bobY);
ctx.rotate(weaponAngle + attackSwing*Math.PI/180);
// Weapon visual based on level
if(p.weaponLevel === 0) {
// Iron Sword - simple blade
ctx.fillStyle=p.weapon.color;
ctx.fillRect(0,-2,16,4);
ctx.fillStyle='#532';
ctx.fillRect(-2,-3,4,6);
} else if(p.weaponLevel === 1) {
// Steel Sword - longer, bluer
ctx.fillStyle=p.weapon.color;
ctx.fillRect(0,-2.5,20,5);
ctx.fillStyle='#8cf';
ctx.fillRect(14,-3,6,6);
ctx.fillStyle='#532';
ctx.fillRect(-2,-4,4,8);
} else {
// Fury Blade - glowing, ornate
const glow = 0.7+Math.sin(game.gameTime*5)*0.3;
ctx.shadowColor=p.weapon.color;ctx.shadowBlur=10;
ctx.fillStyle=p.weapon.color;
ctx.fillRect(0,-3,24,6);
ctx.fillStyle=`rgba(255,100,150,${glow})`;
ctx.fillRect(18,-4,8,8);
ctx.shadowBlur=0;
ctx.fillStyle='#532';
ctx.fillRect(-2,-5,4,10);
}
ctx.restore();
// Speed boots visual (trail effect)
if(p.accessories.some(a=>a.id==='speedBoots')) {
ctx.globalAlpha=0.2;
ctx.fillStyle='#6af';
ctx.beginPath();ctx.arc(sx-3,sy+12+bobY,5,0,Math.PI*2);ctx.fill();
}
// Shield visual
if(p.accessories.some(a=>a.id==='shield')) {
ctx.fillStyle='rgba(139,69,19,0.7)';
const shieldSide = p.facing.x >= 0 ? -1 : 1;
ctx.beginPath();
ctx.moveTo(sx+shieldSide*8-4,sy-2+bobY);
ctx.lineTo(sx+shieldSide*8+4,sy-2+bobY);
ctx.lineTo(sx+shieldSide*8+4,sy+6+bobY);
ctx.lineTo(sx+shieldSide*8,sy+10+bobY);
ctx.lineTo(sx+shieldSide*8-4,sy+6+bobY);
ctx.closePath();ctx.fill();
}
// Power ring glow
if(p.accessories.some(a=>a.id==='powerRing')) {
const rg = 0.3+Math.sin(game.gameTime*4)*0.2;
ctx.fillStyle=`rgba(255,215,0,${rg})`;
ctx.beginPath();ctx.arc(sx,sy+bobY,16,0,Math.PI*2);ctx.fill();
}
ctx.globalAlpha=1;
}
function drawMinimap() {
mctx.fillStyle='#111';
mctx.fillRect(0,0,120,90);
const roomW = 120/5;
const roomH = 90/3;
// Draw room grid (simplified layout)
const roomPositions = [
{x:0,y:1}, // Village
{x:1,y:1}, // Dark Forest
{x:2,y:1}, // Crystal Caverns
{x:1,y:2}, // Merchant's Keep
{x:2,y:2}, // Shadow Fortress
];
roomPositions.forEach((rp,i) => {
const visited = game.player.room === i || rooms[i].items.some(it=>it.collected);
mctx.fillStyle = visited ? (i===game.player.room?'#4a6':'#354') : '#222';
mctx.fillRect(rp.x*roomW+2, rp.y*roomH+2, roomW-4, roomH-4);
if(visited) {
mctx.fillStyle='#889';mctx.font='7px sans-serif';mctx.textAlign='center';
mctx.fillText(rooms[i].name.substring(0,6), rp.x*roomW+roomW/2, rp.y*roomH+roomH/2+3);
}
});
// Draw connections
mctx.strokeStyle='#445';mctx.lineWidth=1;
[[0,1],[1,2],[1,3],[2,4]].forEach(([a,b])=>{
const ra=roomPositions[a],rb=roomPositions[b];
mctx.beginPath();
mctx.moveTo(ra.x*roomW+roomW/2,ra.y*roomH+roomH/2);
mctx.lineTo(rb.x*roomW+roomW/2,rb.y*roomH+roomH/2);
mctx.stroke();
});
// Player dot
const pr = roomPositions[game.player.room];
mctx.fillStyle='#4f4';
mctx.beginPath();
mctx.arc(pr.x*roomW+roomW/2,pr.y*roomH+roomH/2,3,0,Math.PI*2);
mctx.fill();
}
// ==================== MANA REGEN ====================
setInterval(()=>{
if(!game || !game.running) return;
if(game.player.mana < game.player.maxMana && game.player.hasRangedWeapon) {
game.player.mana++;
}
}, 3000);
// Auto-save every 60 seconds
setInterval(()=>{
if(!game || !game.running) return;
saveGame();
}, 60000);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"98886337-6a6c-450d-9d89-3a46c8edb437","tokensIn":74897,"tokensOut":21914,"tokensTotal":96811,"turns":4,"toolCalls":3,"failedToolCalls":0,"timestamp":"2026-07-18T23:53:05.267Z"} -->