RPG
Deepseek v4 Pro · basic

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 23,094 bytes · SHA-256 399acb480435
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>EMBERVEIL: Blade of Echoes · action-adventure RPG</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #1a1e2b;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: 'Segoe UI', 'Arial', sans-serif;
overflow: hidden;
}
#game-container {
position: relative;
width: 960px;
height: 640px;
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
border-radius: 12px;
overflow: hidden;
background: #0b0e14;
}
canvas {
display: block;
width: 100%;
height: 100%;
image-rendering: crisp-edges;
}
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.hud {
position: absolute;
bottom: 20px;
left: 20px;
display: flex;
gap: 12px;
align-items: center;
background: rgba(8, 12, 20, 0.8);
backdrop-filter: blur(8px);
padding: 10px 18px;
border-radius: 40px;
border: 1px solid rgba(220, 180, 80, 0.5);
box-shadow: 0 8px 20px rgba(0,0,0,0.7);
pointer-events: auto;
}
.health-container {
display: flex;
gap: 6px;
}
.heart {
width: 28px;
height: 28px;
background: conic-gradient(from 0deg, #d44, #a11);
clip-path: polygon(50% 15%, 61% 25%, 75% 25%, 85% 38%, 85% 55%, 65% 75%, 50% 88%, 35% 75%, 15% 55%, 15% 38%, 25% 25%, 39% 25%);
background-size: cover;
transition: 0.2s;
filter: drop-shadow(0 0 6px #f44);
}
.heart.empty {
filter: grayscale(1) brightness(0.4);
background: #3a2a2a;
box-shadow: none;
}
.weapon-indicator {
background: #2a2f3c;
border-radius: 20px;
padding: 5px 15px;
color: #fadf9e;
font-weight: bold;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
border: 1px solid #b89b40;
}
.money-display {
background: #3c3a2a;
border-radius: 20px;
padding: 5px 15px;
color: #ffd966;
font-weight: bold;
margin-left: 5px;
border: 1px solid #c9a23b;
}
.inventory-panel {
position: absolute;
bottom: 100px;
right: 20px;
background: rgba(15, 20, 30, 0.9);
backdrop-filter: blur(12px);
border-radius: 20px;
padding: 12px 16px;
border: 1px solid #8b7a4c;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: auto;
min-width: 180px;
}
.inv-title {
color: #d4b87a;
font-size: 14px;
font-weight: bold;
border-bottom: 1px solid #5e4f35;
padding-bottom: 4px;
}
.inv-slots {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.inv-slot {
width: 36px;
height: 36px;
background: #222b35;
border-radius: 8px;
border: 1px solid #4a5a6a;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #ccc;
transition: 0.2s;
}
.inv-slot.filled {
background: #3e4b3a;
border-color: #b9a04b;
box-shadow: 0 0 8px #ab8b3e;
}
.save-indicator {
position: absolute;
top: 20px;
right: 20px;
background: #2d3a4a;
color: #cbdbf7;
padding: 6px 14px;
border-radius: 30px;
font-size: 13px;
border: 1px solid #7f8c9a;
pointer-events: auto;
}
.message-pop {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
background: #0f0f18;
color: #efdba8;
padding: 12px 28px;
border-radius: 40px;
font-weight: bold;
font-size: 18px;
border: 1px solid #dbb45c;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
button {
background: #4e5b4c;
border: none;
color: #f5e7c6;
font-weight: bold;
border-radius: 20px;
padding: 6px 12px;
cursor: pointer;
border: 1px solid #9e8d5c;
transition: 0.2s;
}
button:hover {
background: #6b7a5c;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="960" height="640"></canvas>
<div id="ui-layer">
<div class="hud">
<div class="health-container" id="healthDisplay"></div>
<div class="weapon-indicator" id="weaponDisplay">⚔️ Rusty Blade</div>
<div class="money-display" id="moneyDisplay">🪙 0</div>
</div>
<div class="inventory-panel">
<div class="inv-title">🎒 Inventory</div>
<div class="inv-slots" id="inventorySlots"></div>
</div>
<div class="save-indicator" id="saveIndicator">💾 Save: <span id="saveLocation">None</span></div>
<div class="message-pop" id="messagePopup"></div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ---------- AUDIO SYSTEM (synthesized SFX) ----------
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() {
if (!audioCtx) audioCtx = new AudioCtx();
}
function playSound(freq, type, duration, vol = 0.1, glide = 0) {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t);
if (glide) osc.frequency.linearRampToValueAtTime(freq + glide, t + duration);
gain.gain.setValueAtTime(vol, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + duration);
osc.connect(gain).connect(audioCtx.destination);
osc.start(t);
osc.stop(t + duration);
}
function sfxWalk() { playSound(80, 'triangle', 0.08, 0.03); }
function sfxAttack() { playSound(200, 'sawtooth', 0.15, 0.08, -20); }
function sfxHit() { playSound(70, 'square', 0.12, 0.07); }
function sfxPickup() { playSound(600, 'sine', 0.15, 0.09, 150); }
function sfxShop() { playSound(800, 'triangle', 0.2, 0.1, -100); }
function sfxSave() { playSound(440, 'sine', 0.2, 0.1); setTimeout(()=>playSound(660,'sine',0.15,0.1),120); }
function sfxRanged() { playSound(300, 'triangle', 0.12, 0.06, 500); }
// ---------- GAME STATE ----------
const player = {
x: 200, y: 200,
maxHealth: 6, // 3 points = 3*2 health units (each heart 2 points)
health: 6,
speed: 3.8,
weapon: 'rusty_blade', // base
weaponUpgrades: 0, // 0,1,2
rangedUnlocked: false,
rangedWeapon: null,
inventory: [], // max 8 slots
money: 0,
facing: 1, // 1 right, -1 left
attackTimer: 0,
attackCooldown: 0,
rangedCooldown: 0,
invincible: false,
invincibilityTimer: 0,
};
// Progression flags
let gameProgress = 0; // 0-100 approx, mid at 50 unlocks ranged discovery
let defeatedEnemies = 0;
const maxEnemiesForProgress = 12;
// World objects
let enemies = [];
let items = []; // pickups on ground
let savePoints = [];
let shopActive = false;
let shopLocation = { x: 700, y: 150, radius: 50 };
// Particles
let particles = [];
// Save data
let currentSaveLocation = null;
// Keys
const keys = {
w: false, a: false, s: false, d: false,
ArrowUp: false, ArrowLeft: false, ArrowDown: false, ArrowRight: false,
space: false, e: false, r: false, f: false
};
// ---------- HELPER: update UI ----------
function updateUI() {
const healthDiv = document.getElementById('healthDisplay');
const totalHearts = Math.floor(player.maxHealth / 2);
const filledHearts = Math.floor(player.health / 2);
let html = '';
for (let i = 0; i < totalHearts; i++) {
html += `<div class="heart${i >= filledHearts ? ' empty' : ''}"></div>`;
}
healthDiv.innerHTML = html;
let weaponName = 'Rusty Blade';
if (player.weapon === 'iron_sword') weaponName = '⚔️ Iron Sword';
else if (player.weapon === 'flame_blade') weaponName = '🔥 Flame Blade';
document.getElementById('weaponDisplay').innerHTML = weaponName;
document.getElementById('moneyDisplay').innerHTML = `🪙 ${player.money}`;
const invSlots = document.getElementById('inventorySlots');
invSlots.innerHTML = '';
for (let i = 0; i < 8; i++) {
const item = player.inventory[i];
const slot = document.createElement('div');
slot.className = `inv-slot${item ? ' filled' : ''}`;
slot.innerHTML = item ? item.icon : '○';
invSlots.appendChild(slot);
}
document.getElementById('saveLocation').innerText = currentSaveLocation || 'None';
}
function showMessage(text) {
const pop = document.getElementById('messagePopup');
pop.innerText = text;
pop.style.opacity = '1';
setTimeout(() => { pop.style.opacity = '0'; }, 1800);
}
// ---------- PARTICLES ----------
function spawnParticles(x, y, color, count = 6) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 3,
vy: (Math.random() - 0.5) * 3 - 1,
life: 0.6 + Math.random() * 0.5,
color
});
}
}
// ---------- ENEMIES ----------
function spawnEnemy(x, y) {
return {
x, y,
health: 2,
maxHealth: 2,
speed: 1.2 + Math.random() * 0.8,
damage: 1,
type: 'melee',
cooldown: 0,
};
}
function populateEnemies() {
enemies = [];
for (let i = 0; i < 8; i++) {
enemies.push(spawnEnemy(150 + Math.random() * 600, 100 + Math.random() * 400));
}
}
// ---------- ITEMS & PICKUPS ----------
function createItem(x, y, type) {
const itemsCatalog = {
healthPotion: { name: 'Health Potion', icon: '❤️', effect: ()=> { if(player.health<player.maxHealth){ player.health = Math.min(player.maxHealth, player.health+2); spawnParticles(player.x,player.y,'#f88',8); } } },
staminaCharm: { name: 'Stamina Charm', icon: '💨', effect: ()=> { player.speed += 0.4; showMessage('Speed increased!'); } },
ironOre: { name: 'Iron Ore', icon: '⛏️', effect: ()=> { player.money += 15; } },
goldRing: { name: 'Gold Ring', icon: '💍', effect: ()=> { player.money += 30; } },
defenseAmulet: { name: 'Defense Amulet', icon: '🛡️', effect: ()=> { player.maxHealth = Math.min(10, player.maxHealth+2); player.health = Math.min(player.maxHealth, player.health+2); showMessage('Max Health +1 heart'); } },
magicScroll: { name: 'Magic Scroll', icon: '📜', effect: ()=> { if(!player.rangedUnlocked && gameProgress>=50) { player.rangedUnlocked = true; player.rangedWeapon = 'magic_bolt'; showMessage('Ranged: Magic Bolt unlocked!'); } } },
};
return { x, y, type, ...itemsCatalog[type] };
}
function populateItems() {
items = [
createItem(300, 180, 'healthPotion'),
createItem(480, 300, 'staminaCharm'),
createItem(620, 120, 'ironOre'),
createItem(180, 420, 'goldRing'),
createItem(750, 380, 'defenseAmulet'),
createItem(550, 480, 'magicScroll'),
];
}
// ---------- SAVE POINTS ----------
function createSavePoint(x, y, id) {
return { x, y, id, radius: 28 };
}
function populateSavePoints() {
savePoints = [
createSavePoint(150, 500, 'Ruined Shrine'),
createSavePoint(800, 450, 'Crystal Cavern'),
];
}
// ---------- SHOP ----------
function buyItem(itemId) {
const shopItems = {
ironSword: { cost: 40, action: ()=> {
if(player.weaponUpgrades < 1) { player.weapon = 'iron_sword'; player.weaponUpgrades = 1; showMessage('Weapon upgraded: Iron Sword'); spawnParticles(player.x,player.y,'#b0c4de',12); }
}},
flameBlade: { cost: 80, action: ()=> {
if(player.weaponUpgrades === 1) { player.weapon = 'flame_blade'; player.weaponUpgrades = 2; showMessage('Weapon upgraded: Flame Blade'); spawnParticles(player.x,player.y,'#ff8c00',14); }
}},
healthElixir: { cost: 25, action: ()=> { player.health = Math.min(player.maxHealth, player.health+2); showMessage('Health restored'); } },
};
if (!shopItems[itemId]) return;
if (player.money >= shopItems[itemId].cost) {
player.money -= shopItems[itemId].cost;
shopItems[itemId].action();
sfxShop();
} else {
showMessage('Not enough gold');
}
}
// ---------- COMBAT & DAMAGE ----------
function playerAttack() {
if (player.attackCooldown > 0) return;
player.attackCooldown = 18;
sfxAttack();
let damage = 1;
let range = 45;
if (player.weapon === 'iron_sword') { damage = 2; range = 55; }
if (player.weapon === 'flame_blade') { damage = 3; range = 50; }
const attackDir = player.facing;
const hitX = player.x + attackDir * 25;
const hitY = player.y;
spawnParticles(hitX, hitY, player.weapon==='flame_blade'?'#ff6600':'#ddd', 5);
enemies.forEach((enemy, idx) => {
const dx = enemy.x - hitX;
const dy = enemy.y - hitY;
if (Math.sqrt(dx*dx+dy*dy) < range) {
enemy.health -= damage;
spawnParticles(enemy.x, enemy.y, '#f44', 4);
sfxHit();
if (enemy.health <= 0) {
enemies.splice(idx,1);
defeatedEnemies++;
player.money += 10;
updateProgress();
}
}
});
}
function playerRangedAttack() {
if (!player.rangedUnlocked || player.rangedCooldown > 0) return;
player.rangedCooldown = 35;
sfxRanged();
const boltX = player.x + player.facing * 20;
const boltY = player.y;
spawnParticles(boltX, boltY, '#7df9ff', 8);
enemies.forEach((enemy, idx) => {
const dx = enemy.x - boltX;
const dy = enemy.y - boltY;
if (Math.abs(dx) < 80 && Math.abs(dy) < 40) {
enemy.health -= 2;
spawnParticles(enemy.x, enemy.y, '#0ff', 6);
if (enemy.health <= 0) {
enemies.splice(idx,1);
defeatedEnemies++;
player.money += 12;
updateProgress();
}
}
});
}
function updateProgress() {
gameProgress = Math.min(100, Math.floor((defeatedEnemies / maxEnemiesForProgress) * 60 + (player.weaponUpgrades*15) + (player.rangedUnlocked?20:0)));
if (gameProgress >= 50 && !player.rangedUnlocked && player.inventory.some(i=>i&&i.type==='magicScroll')) {
// auto unlock possible via scroll pickup
}
}
// ---------- PLAYER MOVEMENT & COLLISION ----------
function movePlayer() {
let dx = 0, dy = 0;
if (keys.w || keys.ArrowUp) dy -= 1;
if (keys.s || keys.ArrowDown) dy += 1;
if (keys.a || keys.ArrowLeft) dx -= 1;
if (keys.d || keys.ArrowRight) dx += 1;
if (dx !== 0 || dy !== 0) {
const len = Math.sqrt(dx*dx+dy*dy);
dx /= len; dy /= len;
player.x += dx * player.speed;
player.y += dy * player.speed;
if (dx !== 0) player.facing = dx > 0 ? 1 : -1;
if (Math.random() < 0.3) sfxWalk();
}
player.x = Math.max(25, Math.min(935, player.x));
player.y = Math.max(25, Math.min(615, player.y));
}
function checkItemPickup() {
for (let i = items.length-1; i >= 0; i--) {
const it = items[i];
const d = Math.hypot(player.x - it.x, player.y - it.y);
if (d < 30) {
if (player.inventory.length < 8) {
player.inventory.push(it);
items.splice(i,1);
sfxPickup();
spawnParticles(it.x, it.y, '#ffd700', 8);
if (it.effect) it.effect();
updateProgress();
} else {
showMessage('Inventory full');
}
}
}
}
function checkSaveInteraction() {
if (keys.e) {
for (let sp of savePoints) {
if (Math.hypot(player.x - sp.x, player.y - sp.y) < sp.radius) {
currentSaveLocation = sp.id;
sfxSave();
showMessage('Game saved at ' + sp.id);
keys.e = false;
}
}
}
}
function checkShopInteraction() {
const d = Math.hypot(player.x - shopLocation.x, player.y - shopLocation.y);
if (d < shopLocation.radius && keys.f) {
shopActive = true;
keys.f = false;
showShopDialog();
}
}
function showShopDialog() {
const shopHTML = `
<div style="position:absolute; top:150px; left:280px; background:#1e2330; border:2px solid #c9a03b; border-radius:24px; padding:20px; color:#eee; z-index:100; pointer-events:auto;">
<h3>🏪 Traveling Merchant</h3>
<button id="buyIronSword">Iron Sword (40g)</button>
<button id="buyFlameBlade">Flame Blade (80g)</button>
<button id="buyHealth">Health Elixir (25g)</button>
<button id="closeShop">Close</button>
</div>
`;
const div = document.createElement('div');
div.innerHTML = shopHTML;
document.getElementById('ui-layer').appendChild(div);
document.getElementById('buyIronSword').onclick = ()=>{ buyItem('ironSword'); closeShopUI(); };
document.getElementById('buyFlameBlade').onclick = ()=>{ buyItem('flameBlade'); closeShopUI(); };
document.getElementById('buyHealth').onclick = ()=>{ buyItem('healthElixir'); closeShopUI(); };
document.getElementById('closeShop').onclick = closeShopUI;
}
function closeShopUI() {
const shops = document.querySelectorAll('#ui-layer > div:last-child');
if (shops.length) shops[shops.length-1].remove();
shopActive = false;
}
// ---------- ENEMY AI ----------
function updateEnemies() {
enemies.forEach(enemy => {
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const dist = Math.hypot(dx, dy);
if (dist < 200 && dist > 25) {
enemy.x += (dx/dist) * enemy.speed;
enemy.y += (dy/dist) * enemy.speed;
}
if (dist < 30 && enemy.cooldown <= 0 && !player.invincible) {
player.health -= enemy.damage;
player.invincible = true;
player.invincibilityTimer = 40;
spawnParticles(player.x, player.y, '#f00', 6);
sfxHit();
enemy.cooldown = 35;
}
if (enemy.cooldown > 0) enemy.cooldown--;
});
}
// ---------- GAME LOOP ----------
function update() {
movePlayer();
if (player.attackCooldown > 0) player.attackCooldown--;
if (player.rangedCooldown > 0) player.rangedCooldown--;
if (player.invincibilityTimer > 0) {
player.invincibilityTimer--;
if (player.invincibilityTimer <= 0) player.invincible = false;
}
if (keys.space) { playerAttack(); keys.space = false; }
if (keys.r) { playerRangedAttack(); keys.r = false; }
checkItemPickup();
checkSaveInteraction();
checkShopInteraction();
updateEnemies();
// particles update
for (let i=particles.length-1;i>=0;i--) {
particles[i].x += particles[i].vx;
particles[i].y += particles[i].vy;
particles[i].life -= 0.02;
if (particles[i].life <= 0) particles.splice(i,1);
}
if (player.health <= 0) {
player.health = player.maxHealth;
player.x = 200; player.y = 200;
showMessage('You fell... respawned');
}
updateUI();
}
function draw() {
ctx.clearRect(0,0,960,640);
// ground
ctx.fillStyle = '#2d3a2a';
ctx.fillRect(0,0,960,640);
for (let i=0;i<20;i++) {
ctx.fillStyle = '#3f5532';
ctx.fillRect(i*60,0,30,640);
}
// save points
savePoints.forEach(sp => {
ctx.fillStyle = '#5b7a9a';
ctx.beginPath();
ctx.arc(sp.x, sp.y, sp.radius, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = '12px sans-serif';
ctx.fillText(sp.id, sp.x-25, sp.y-20);
});
// shop
ctx.fillStyle = '#b8863b';
ctx.beginPath(); ctx.arc(shopLocation.x, shopLocation.y, shopLocation.radius, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff'; ctx.fillText('Shop', shopLocation.x-20, shopLocation.y-18);
// items
items.forEach(it => {
ctx.fillStyle = '#f5e56c';
ctx.beginPath(); ctx.arc(it.x, it.y, 12, 0, 2*Math.PI); ctx.fill();
ctx.fillStyle = '#000'; ctx.font='14px sans-serif'; ctx.fillText(it.icon, it.x-8, it.y+6);
});
// enemies
enemies.forEach(e => {
ctx.fillStyle = '#c44';
ctx.fillRect(e.x-12, e.y-12, 24, 24);
});
// player
ctx.save();
ctx.translate(player.x, player.y);
if (player.facing === -1) ctx.scale(-1,1);
ctx.fillStyle = '#3b7dd8';
ctx.fillRect(-14, -14, 28, 28);
if (player.weapon === 'flame_blade') ctx.fillStyle = '#ff8c00';
else if (player.weapon === 'iron_sword') ctx.fillStyle = '#a0b0c0';
else ctx.fillStyle = '#8b6b4d';
ctx.fillRect(12, -6, 16, 6);
ctx.restore();
// particles
particles.forEach(p => {
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill();
});
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// INPUT
window.addEventListener('keydown', e => {
if (e.key in keys) { keys[e.key] = true; e.preventDefault(); }
if (e.key === ' ' || e.key === 'Space') { keys.space = true; e.preventDefault(); }
if (e.key === 'e') keys.e = true;
if (e.key === 'r') keys.r = true;
if (e.key === 'f') keys.f = true;
});
window.addEventListener('keyup', e => {
if (e.key in keys) { keys[e.key] = false; e.preventDefault(); }
if (e.key === ' ' || e.key === 'Space') keys.space = false;
if (e.key === 'e') keys.e = false;
if (e.key === 'r') keys.r = false;
if (e.key === 'f') keys.f = false;
});
// INIT
populateEnemies();
populateItems();
populateSavePoints();
updateUI();
initAudio();
gameLoop();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4-pro","provider":"openrouter","persona":"basic","sessionId":"4dc179be-3007-4394-9294-9f9d7b928231","tokensIn":707,"tokensOut":6881,"tokensTotal":7588,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T18:06:23.588Z"} -->