RPG
inkling · 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 39,844 bytes · SHA-256 0389133070bd
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shard of Eternity — Action RPG</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700&family=Cinzel:wght@600&family=Merriweather:ital,wght@0,300;0,400;1,400&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0b0c10;
color: #e8e6e3;
font-family: 'Merriweather', Georgia, serif;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
#gameWrap {
position: relative;
width: 1024px;
height: 720px;
max-width: 100vw;
max-height: 100vh;
background: linear-gradient(135deg, #0f1115 0%, #181a20 60%, #22242a 100%);
border: 2px solid #333;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 0 60px rgba(0,0,0,0.9), inset 0 0 120px rgba(200,180,120,0.03);
}
canvas { display: block; width: 100%; height: 100%; }
#hud {
position: absolute;
top: 0; left: 0; right: 0;
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
pointer-events: none;
z-index: 10;
background: linear-gradient(to bottom, rgba(10,11,16,0.92) 0%, rgba(10,11,16,0.45) 70%, transparent);
}
.hud-group { display: flex; align-items: center; gap: 16px; }
.hearts {
display: flex; gap: 4px; align-items: center;
}
.heart {
width: 22px; height: 20px;
position: relative;
filter: drop-shadow(0 0 3px rgba(255,60,90,0.4));
}
.heart svg { display: block; width: 100%; height: 100%; }
.heart.full path { fill: #ff3b5a; stroke: #c0203a; stroke-width: 1; }
.heart.half path { fill: url(#halfGrad); stroke: #ff8a9e; stroke-width: 1; }
.heart.empty path { fill: #2a2a32; stroke: #444; stroke-width: 1; }
.money {
display: flex; align-items: center; gap: 6px;
font-family: 'Cinzel', serif; font-size: 16px; color: #f0c040;
letter-spacing: 1px; text-shadow: 0 0 6px rgba(240,192,64,0.3);
}
.money-icon {
width: 18px; height: 18px; border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #ffe066 0%, #d4a01a 60%, #7a5a10 100%);
box-shadow: inset -2px -2px 3px rgba(0,0,0,0.4), 0 0 4px rgba(240,192,64,0.3);
}
.weapon-badge {
display: flex; align-items: center; gap: 6px;
font-family: 'Cinzel', serif; font-size: 13px; color: #c0b0a0;
background: rgba(255,255,255,0.05); padding: 4px 10px; border-radius: 20px;
border: 1px solid rgba(255,255,255,0.08);
}
.weapon-icon { width: 16px; height: 16px; border-radius: 2px; background: #ccc; box-shadow: 0 0 4px rgba(255,255,255,0.3); }
#overlay {
position: absolute; inset: 0; z-index: 20;
display: flex; align-items: center; justify-content: center;
pointer-events: none;
}
#startScreen, #gameOverScreen, #shopUI {
pointer-events: auto;
background: rgba(10,12,18,0.92);
border: 2px solid rgba(200,180,130,0.2);
border-radius: 16px;
padding: 48px 64px;
max-width: 520px;
text-align: center;
box-shadow: 0 0 80px rgba(0,0,0,0.6), inset 0 0 40px rgba(200,180,120,0.05);
}
#startScreen h1 {
font-family: 'Cinzel Decorative', serif; font-size: 42px; color: #e8dcc8;
text-shadow: 0 0 30px rgba(200,180,120,0.35), 0 2px 8px rgba(0,0,0,0.9);
margin-bottom: 12px; letter-spacing: 3px;
}
#startScreen .subtitle {
font-family: 'Cinzel', serif; font-size: 16px; color: #b0a898;
margin-bottom: 28px; letter-spacing: 2px;
}
#startScreen p { color: #aaa8a0; line-height: 1.7; margin-bottom: 24px; font-size: 15px; }
#startScreen .controls { color: #a89f88; font-size: 13px; margin-bottom: 28px; line-height: 1.6; }
.btn {
display: inline-block; padding: 14px 36px; border-radius: 8px;
background: linear-gradient(180deg, #c0a460 0%, #8a6840 100%);
color: #1a150e; font-family: 'Cinzel', serif; font-weight: 700; font-size: 18px;
border: none; cursor: pointer; letter-spacing: 2px; box-shadow: 0 4px 20px rgba(200,160,80,0.25), 0 6px 0 #5a4428;
transition: transform .1s, box-shadow .2s; text-shadow: 0 1px 1px rgba(255,255,255,0.15);
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(200,160,80,0.35), 0 9px 0 #5a4428; }
.btn:active { transform: translateY(2px); box-shadow: 0 2px 8px rgba(200,160,80,0.2), 0 3px 0 #5a4428; }
#gameOverScreen h2 {
font-family: 'Cinzel Decorative', serif; font-size: 36px; color: #e8c8c0;
text-shadow: 0 0 20px rgba(240,120,120,0.3); margin-bottom: 16px;
}
#gameOverScreen p { color: #b8a898; font-size: 16px; margin-bottom: 24px; }
#shopUI { pointer-events: auto; display: none; }
#shopUI h2 { font-family: 'Cinzel Decorative', serif; font-size: 28px; color: #c0b0a0; margin-bottom: 20px; text-shadow: 0 0 16px rgba(200,180,150,0.2); }
#shopUI .items { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.shop-item {
display: flex; align-items: center; gap: 14px; padding: 12px 16px;
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px; text-align: left; transition: background .15s;
}
.shop-item:hover { background: rgba(255,255,255,0.08); }
.shop-item .icon { width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 18px; box-shadow: 0 0 8px rgba(0,0,0,0.4); }
.shop-item .info { flex: 1; }
.shop-item .name { font-family: 'Cinzel', serif; font-size: 15px; color: #e0dcc8; }
.shop-item .desc { font-size: 12px; color: #9a9588; }
.shop-item .price { font-family: 'Cinzel', serif; color: #f0c040; font-weight: 600; min-width: 70px; text-align: right; }
#shopUI .close-btn { margin-top: 20px; background: linear-gradient(180deg, #6a7a8a 0%, #3a4555 100%); color: #d8d8d8; padding: 10px 28px; font-family: 'Cinzel', serif; border-radius: 6px; border: none; cursor: pointer; box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
#shopUI .close-btn:hover { transform: translateY(-1px); }
.damage-float {
position: absolute; pointer-events: none; z-index: 30;
font-family: 'Cinzel', serif; font-weight: 700; font-size: 18px;
color: #ff6b6b; text-shadow: 0 0 4px rgba(255,0,0,0.4), 1px 1px 0 rgba(0,0,0,0.6);
animation: floatUp .8s ease-out forwards;
}
@keyframes floatUp {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(-30px) scale(0.8); }
}
#saveMsg {
position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
background: rgba(200,160,80,0.15); border: 1px solid rgba(200,160,80,0.3);
padding: 8px 20px; border-radius: 20px; color: #e8c898; font-family: 'Cinzel', serif; font-size: 14px; pointer-events: none; opacity: 0; transition: opacity .3s; z-index: 25;
}
#inventoryBar {
position: absolute; bottom: 0; left: 0; right: 0;
height: 64px; background: linear-gradient(to top, rgba(10,11,16,0.95) 0%, rgba(10,11,16,0.4) 100%);
display: flex; align-items: center; justify-content: center; gap: 14px; padding: 0 24px; z-index: 10;
}
.inv-slot {
width: 48px; height: 48px; border-radius: 10px; border: 2px solid rgba(255,255,255,0.1);
background: rgba(255,255,255,0.03); display: flex; align-items: center; justify-content: center; position: relative;
transition: border-color .15s, background .15s;
}
.inv-slot.active { border-color: rgba(240,192,64,0.5); background: rgba(240,192,64,0.08); box-shadow: 0 0 8px rgba(240,192,64,0.15); }
.inv-slot .qty { position: absolute; bottom: 2px; right: 4px; font-size: 9px; color: #aaa; font-family: 'Cinzel', serif; }
.inv-icon { width: 28px; height: 28px; border-radius: 50%; box-shadow: 0 0 6px rgba(0,0,0,0.5); }
/* Particle canvas overlay for effects */
#fxCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5; }
</style>
</head>
<body>
<div id="gameWrap">
<canvas id="world"></canvas>
<canvas id="fxCanvas"></canvas>
<div id="hud">
<div class="hud-group">
<div class="hearts" id="hearts">
<!-- Hearts generated by JS -->
</div>
</div>
<div class="hud-group" style="justify-content:center; flex:1; justify-content:center;">
<div class="weapon-badge" id="weaponBadge">
<div class="weapon-icon" id="weaponIcon"></div>
<span id="weaponName">Iron Blade</span>
</div>
</div>
<div class="hud-group">
<div class="money"><div class="money-icon"></div><span id="moneyText">0 G</span></div>
</div>
</div>
<div id="inventoryBar">
<!-- Slots generated -->
</div>
<div id="overlay">
<div id="startScreen">
<h1>Shard of Eternity</h1>
<div class="subtitle">A Short Action Adventure</div>
<p>
Deep within the forgotten ruins, a crystal shard pulses with ancient light.<br>
Equip your blade, grow stronger, and reclaim the lost treasure.
</p>
<div class="controls">
<b>WASD / Arrow Keys</b> — Move<br>
<b>Left Click / J</b> — Attack<br>
<b>Right Click / K</b> — Ranged (mid-game)<br>
<b>Space / Enter</b> — Interact (shop, save, pickup)<br>
<b>1-5</b> — Use consumable
</div>
<button class="btn" onclick="startGame()">Begin Journey</button>
</div>
<div id="shopUI">
<h2>Merchant of the Ruin</h2>
<div class="items" id="shopItems"></div>
<button class="close-btn" onclick="closeShop()">Leave</button>
</div>
<div id="gameOverScreen" style="display:none;">
<h2>Fallen Hero</h2>
<p>You have fallen in the ruins. Your spirit lingers, but the shard remains.<br>Try again — stronger this time.</p>
<button class="btn" onclick="startGame()">Respawn</button>
</div>
</div>
<div id="saveMsg">Progress saved to crystal.</div>
</div>
<svg style="position:absolute; width:0; height:0; overflow:hidden;" aria-hidden="true">
<defs>
<linearGradient id="halfGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="50%" stop-color="#ff3b5a"/>
<stop offset="50%" stop-color="#2a2a32"/>
</linearGradient>
</defs>
</svg>
<script>
// ======================= SOUND ENGINE =======================
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() { if (!audioCtx) audioCtx = new AudioCtx(); if (audioCtx.state === 'suspended') audioCtx.resume(); }
function playSound(type, options={}) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
const freq = options.freq || 400;
osc.type = options.type || 'triangle';
osc.frequency.setValueAtTime(freq, now);
if (options.sweep) osc.frequency.exponentialRampToValueAtTime(options.sweep, now + 0.1);
gain.gain.setValueAtTime(options.vol || 0.06, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + options.dur || 0.15);
osc.start(now);
osc.stop(now + (options.dur || 0.15));
}
function soundAttack() { playSound('attack', { freq: 500, type: 'square', vol: 0.06, dur: 0.12 }); }
function soundHit() { playSound('hit', { freq: 220, type: 'sine', vol: 0.07, dur: 0.2, sweep: 80 }); }
function soundPickup() { playSound('pickup', { freq: 700, type: 'triangle', vol: 0.05, dur: 0.25, sweep: 1200 }); playSound('pickup2', { freq: 950, type: 'triangle', vol: 0.03, dur: 0.3, sweep: 1600 }); }
function soundDeath() { playSound('death', { freq: 300, type: 'sawtooth', vol: 0.08, dur: 0.5, sweep: 50 }); }
function soundSave() { playSound('save', { freq: 900, type: 'sine', vol: 0.05, dur: 0.3, sweep: 1400 }); }
function soundStep() { playSound('step', { freq: 150, type: 'sine', vol: 0.015, dur: 0.08 }); }
function soundShop() { playSound('shop', { freq: 600, type: 'triangle', vol: 0.05, dur: 0.4, sweep: 400 }); }
function soundUpgrade() { playSound('upgrade', { freq: 550, type: 'triangle', vol: 0.07, dur: 0.35, sweep: 1100 }); playSound('upgrade2', { freq: 880, type: 'triangle', vol: 0.05, dur: 0.35, sweep: 1400 }); }
function soundBow() { playSound('bow', { freq: 650, type: 'sine', vol: 0.06, dur: 0.2, sweep: 300 }); }
// ======================= GAME STATE =======================
const canvas = document.getElementById('world');
const ctx = canvas.getContext('2d');
const fxCanvas = document.getElementById('fxCanvas');
const fxCtx = fxCanvas.getContext('2d');
let W = 1024, H = 720;
let gameState = 'title'; // title, playing, dead
let keys = {};
let mouse = { x: 0, y: 0, down: false };
// Player
const player = {
x: 420, y: 340,
vx: 0, vy: 0,
speed: 2.4,
health: 3,
maxHealth: 3,
healthTarget: 3,
money: 0,
weaponLevel: 0, // 0 basic, 1 upgraded (steel), 2 master (void)
rangedUnlocked: false,
inventory: [],
facing: 1,
invuln: 0,
stepTimer: 0,
attackCooldown: 0,
attackAnim: 0,
};
// Weapon definitions
const weapons = [
{ name: "Iron Blade", dmg: 8, speed: 0.55, range: 40, iconColor: "#aaa", desc: "Basic sword" },
{ name: "Steel Edge", dmg: 16, speed: 0.40, range: 55, iconColor: "#7cbcf0", desc: "Upgraded blade" },
{ name: "Void Scythe", dmg: 28, speed: 0.30, range: 70, iconColor: "#c080f0", desc: "Master weapon" },
];
// Inventory items (>=5 distinct)
const itemDefs = [
{ id: 'potion', name: 'Health Potion', desc: 'Restores 2 HP', icon: '#ff6b6b', qty: 1, price: 30, use: () => { if (player.health < player.maxHealth) { player.health = Math.min(player.maxHealth, player.health + 2); spawnText('+2 HP', player.x, player.y - 20, '#6bff6b'); soundPickup(); } } },
{ id: 'shield', name: 'Crystal Shield', desc: 'Blocks next hit', icon: '#6bbfff', qty: 1, price: 50, passive: true, effect: () => { /* handled by shield flag */ } },
{ id: 'ring', name: 'Swift Ring', desc: '+15% speed', icon: '#ffe066', qty: 1, price: 40, passive: true },
{ id: 'crystal', name: 'Power Crystal', desc: '+3 damage (temp)', icon: '#c080f0', qty: 2, price: 60, use: () => { player.tempDmg = (player.tempDmg||0) + 3; spawnText('+3 DMG', player.x, player.y - 20, '#c080f0'); soundPickup(); } },
{ id: 'heartshard', name: 'Heart Shard', desc: '+1 Max Health', icon: '#ff3b5a', qty: 1, price: 0, use: () => { if (player.maxHealth < 5) { player.maxHealth = Math.min(5, player.maxHealth + 1); player.health = player.maxHealth; spawnText('Max HP +1!', player.x, player.y - 20, '#ff3b5a'); soundUpgrade(); } } },
{ id: 'orb', name: 'Gold Orb', desc: 'Worth 25 gold', icon: '#e8c050', qty: 3, price: 0, use: () => { player.money += 25; spawnText('+25 G', player.x, player.y - 20, '#ffe066'); soundPickup(); } },
];
// World entities
let entities = [];
let particles = [];
let textFloats = [];
// Save points
let savePoints = [
{ x: 200, y: 200, active: true },
{ x: 780, y: 420, active: false },
{ x: 600, y: 620, active: false },
];
let lastSave = { x: 420, y: 340 };
// Zones
const zones = [
{ x: 0, y: 0, w: 340, h: 260, type: 'start' },
{ x: 340, y: 0, w: 280, h: 260, type: 'forest' },
{ x: 620, y: 0, w: 404, h: 260, type: 'ruin' },
{ x: 0, y: 260, w: 340, h: 460, type: 'shop_zone' },
{ x: 340, y: 260, w: 280, h: 460, type: 'mid' },
{ x: 620, y: 260, w: 404, h: 460, type: 'cave' },
];
// Shop position
let shopPos = { x: 450, y: 500 };
// ======================= SETUP =======================
function resize() {
const wrap = document.getElementById('gameWrap');
const rect = wrap.getBoundingClientRect();
W = rect.width; H = rect.height;
canvas.width = W; canvas.height = H;
fxCanvas.width = W; fxCanvas.height = H;
}
window.addEventListener('resize', resize);
// ======================= DRAWING =======================
function drawWorld() {
// Background tiles
const tileSize = 64;
const rows = Math.ceil(W / tileSize) + 2;
const cols = Math.ceil(H / tileSize) + 2;
const offX = (player.x - W/2) % tileSize;
const offY = (player.y - H/2) % tileSize;
for (let r = 0; r < cols; r++) {
for (let c = 0; c < rows; c++) {
const x = c * tileSize - offX - tileSize;
const y = r * tileSize - offY - tileSize;
// Determine zone color
let color = '#161820';
if (y + H/2 > 260) color = '#1a1e2a';
else if (x + W/2 > 620) color = '#181a22';
else if (x + W/2 > 340) color = '#1a1d24';
// Subtle grid
ctx.fillStyle = color;
ctx.fillRect(x, y, tileSize, tileSize);
ctx.strokeStyle = 'rgba(255,255,255,0.015)';
ctx.strokeRect(x, y, tileSize, tileSize);
}
}
// Save crystals
savePoints.forEach(s => {
const sx = s.x - player.x + W/2;
const sy = s.y - player.y + H/2;
if (sx < -30 || sx > W+30 || sy < -30 || sy > H+30) return;
ctx.save();
ctx.translate(sx, sy);
const glow = 15 + Math.sin(Date.now()/300)*5;
ctx.shadowColor = s.active ? '#c0a460' : '#777';
ctx.shadowBlur = glow;
ctx.fillStyle = s.active ? '#e8c898' : '#888';
ctx.beginPath();
// Crystal shape
for (let i = 0; i < 6; i++) {
const a = (i/6)*Math.PI*2;
const r = 18 + (i%2 ? 6 : 2);
const px = Math.cos(a) * r, py = Math.sin(a) * r;
if (i === 0) ctx.moveTo(px, -py);
else ctx.lineTo(px, -py);
}
ctx.closePath();
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.stroke();
ctx.restore();
// Label
ctx.fillStyle = '#aaa';
ctx.font = '10px Cinzel, serif';
ctx.textAlign = 'center';
ctx.fillText('SAVE', sx, sy + 36);
});
// Shop stall
const shopX = shopPos.x - player.x + W/2;
const shopY = shopPos.y - player.y + H/2;
if (shopX > -60 && shopX < W+60 && shopY > -60 && shopY < H+60) {
// Stall base
ctx.save();
ctx.translate(shopX, shopY);
ctx.shadowColor = '#8a6840';
ctx.shadowBlur = 20;
// Roof
ctx.fillStyle = '#5a4428';
ctx.beginPath();
ctx.moveTo(-40, -10); ctx.lineTo(40, -10); ctx.lineTo(50, -30); ctx.lineTo(-50, -30); ctx.closePath();
ctx.fill();
// Pole
ctx.fillStyle = '#8a6840';
ctx.fillRect(-4, -10, 8, 30);
// Banner
ctx.fillStyle = '#d4a050';
ctx.fillRect(-35, -28, 70, 8);
ctx.fillStyle = '#3a2810';
ctx.font = '10px Cinzel, serif';
ctx.textAlign = 'center';
ctx.fillText('SHOP', 0, -20);
ctx.restore();
}
// Enemies
entities.forEach(e => {
if (e.type === 'enemy') {
const ex = e.x - player.x + W/2;
const ey = e.y - player.y + H/2;
if (ex < -40 || ex > W+40 || ey < -40 || ey > H+40) return;
ctx.save();
ctx.translate(ex, ey);
ctx.rotate(Math.PI/8 * Math.sin(Date.now()/200));
// Body
ctx.fillStyle = e.hp > 15 ? '#4a2a1a' : '#8a4a3a';
ctx.beginPath();
ctx.arc(0, 0, 16 + e.hp/3, 0, Math.PI*2);
ctx.fill();
// Eyes (glowing)
ctx.fillStyle = e.hp > 10 ? '#ff5050' : '#ff0000';
ctx.shadowColor = '#ff3030';
ctx.shadowBlur = 8;
ctx.beginPath(); ctx.arc(-6, -3, 3, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(6, -3, 3, 0, Math.PI*2); ctx.fill();
ctx.shadowBlur = 0;
// Health bar
ctx.fillStyle = '#2a1a0a';
ctx.fillRect(-18, -22, 36, 4);
ctx.fillStyle = e.hp > 5 ? '#e06040' : '#ff3030';
ctx.fillRect(-18, -22, 36 * (e.hp / e.maxHp), 4);
ctx.restore();
} else if (e.type === 'drop') {
const dx = e.x - player.x + W/2;
const dy = e.y - player.y + H/2;
if (dx < -20 || dx > W+20 || dy < -20 || dy > H+20) return;
ctx.save();
ctx.translate(dx, dy);
ctx.shadowColor = e.itemColor || '#ffe066';
ctx.shadowBlur = 10;
ctx.fillStyle = e.itemColor || '#ffe066';
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
}
});
// Projectiles
entities.forEach(e => {
if (e.type === 'projectile') {
const px = e.x - player.x + W/2;
const py = e.y - player.y + H/2;
ctx.save();
ctx.translate(px, py);
ctx.fillStyle = '#a0d0ff';
ctx.shadowColor = '#80c0ff';
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.arc(0, 0, 5, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
});
// Player
const px = W/2; const py = H/2;
ctx.save();
ctx.translate(px, py);
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.ellipse(0, 10, 14, 7, 0, 0, Math.PI*2);
ctx.fill();
// Weapon
const wData = weapons[player.weaponLevel];
const angle = Math.atan2(mouse.y - py, mouse.x - px);
const reach = wData.range;
ctx.save();
ctx.rotate(angle);
// Blade
const bladeGrad = ctx.createLinearGradient(0, 0, reach, -5);
bladeGrad.addColorStop(0, '#ddd');
bladeGrad.addColorStop(0.4, wData.iconColor);
bladeGrad.addColorStop(1, '#fff');
ctx.fillStyle = bladeGrad;
ctx.fillRect(0, -4, reach, 8);
ctx.fillStyle = '#fff';
ctx.fillRect(reach - 8, -3, 8, 6);
// Swing indicator on attack
if (player.attackAnim > 0) {
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.beginPath();
ctx.arc(reach/2, 0, reach*0.6, -0.6, 0.6);
ctx.fill();
player.attackAnim -= 0.08;
}
ctx.restore();
// Body
ctx.fillStyle = '#d0c8b8';
ctx.beginPath();
ctx.arc(0, 0, 14, 0, Math.PI*2);
ctx.fill();
// Head
ctx.fillStyle = '#e8dcc8';
ctx.beginPath();
ctx.arc(0, -6, 10, 0, Math.PI*2);
ctx.fill();
// Eyes
ctx.fillStyle = '#333';
const eyeDir = Math.sign(Math.cos(angle)) || 1;
ctx.beginPath();
ctx.arc(3 * eyeDir, -7, 2, 0, Math.PI*2);
ctx.fill();
// Armor glow if shield
if (player.inventory.some(i => i.id === 'shield' && i.passive)) {
ctx.strokeStyle = 'rgba(100,160,255,0.35)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(0, 0, 19, 0, Math.PI*2);
ctx.stroke();
}
ctx.restore();
// Particles
particles.forEach(p => {
p.x += p.vx; p.y += p.vy;
p.life -= p.decay;
if (p.life <= 0) return;
const px = p.x - player.x + W/2;
const py = p.y - player.y + H/2;
ctx.save();
ctx.translate(px, py);
ctx.globalAlpha = p.life / p.maxLife;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(0, 0, p.size, 0, Math.PI*2);
ctx.fill();
ctx.restore();
});
particles = particles.filter(p => p.life > 0);
// Floating texts
textFloats.forEach(t => {
t.y -= 0.6; t.life -= 0.015;
if (t.life <= 0) return;
const tx = t.x - player.x + W/2;
const ty = t.y - player.y + H/2;
ctx.save();
ctx.globalAlpha = t.life;
ctx.fillStyle = t.color;
ctx.font = 'bold 14px Cinzel, serif';
ctx.textAlign = 'center';
ctx.fillText(t.text, tx, ty);
ctx.restore();
});
textFloats = textFloats.filter(t => t.life > 0);
}
// ======================= UPDATE =======================
function spawnText(text, x, y, color) {
textFloats.push({ text, x, y, color: color || '#fff', life: 1 });
}
function spawnParticles(x, y, count, color) {
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
particles.push({
x, y,
vx: Math.cos(a) * (1 + Math.random()*2),
vy: Math.sin(a) * (1 + Math.random()*2),
life: 1,
maxLife: 1,
decay: 0.02 + Math.random()*0.02,
size: 2 + Math.random()*3,
color
});
}
}
function spawnDrop(x, y) {
// Random drop
const roll = Math.random();
let dropItem = null;
if (roll < 0.1) dropItem = { id: 'potion', ...itemDefs.find(i => i.id === 'potion') };
else if (roll < 0.2) dropItem = { id: 'crystal', ...itemDefs.find(i => i.id === 'crystal') };
else if (roll < 0.35) dropItem = { id: 'shield', ...itemDefs.find(i => i.id === 'shield') };
else if (roll < 0.45) dropItem = { id: 'ring', ...itemDefs.find(i => i.id === 'ring') };
else {
dropItem = { id: 'orb', ...itemDefs.find(i => i.id === 'orb') };
}
entities.push({ type: 'drop', x, y, item: dropItem, itemColor: dropItem.icon, life: 300 });
}
function spawnEnemy(x, y, hp = 20) {
entities.push({ type: 'enemy', x, y, hp, maxHp: hp, vx: 0, vy: 0, speed: 0.8, timer: 0 });
}
function update() {
if (gameState !== 'playing') return;
// Player movement
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;
const len = Math.hypot(dx, dy);
if (len > 0) { dx /= len; dy /= len; }
const boost = player.inventory.some(i => i.passive && i.id === 'ring') ? 1.15 : 1;
player.vx = dx * player.speed * boost;
player.vy = dy * player.speed * boost;
player.x += player.vx;
player.y += player.vy;
if (player.x < 20) player.x = 20;
if (player.x > 960) player.x = 960;
if (player.y < 20) player.y = 20;
if (player.y > 680) player.y = 680;
player.facing = dx !== 0 ? Math.sign(dx) : player.facing;
// Step sound
if (len > 0) {
player.stepTimer--;
if (player.stepTimer <= 0) {
player.stepTimer = 15;
soundStep();
}
}
// Attack cooldown
if (player.attackCooldown > 0) player.attackCooldown--;
// Mouse attack
if (mouse.down && player.attackCooldown <= 0) {
player.attackCooldown = weapons[player.weaponLevel].speed * 60;
player.attackAnim = 1;
soundAttack();
const ang = Math.atan2(mouse.y - H/2, mouse.x - W/2);
const reach = weapons[player.weaponLevel].range;
// Hit check
entities.forEach(e => {
if (e.type === 'enemy') {
const dist = Math.hypot(e.x - player.x, e.y - player.y);
const dir = Math.atan2(e.y - player.y, e.x - player.x);
if (dist < reach + 16 && Math.abs(ang - dir) < 0.8) {
const dmg = weapons[player.weaponLevel].dmg + (player.tempDmg || 0);
e.hp -= dmg;
spawnParticles(e.x, e.y, 8, '#f0a040');
spawnText('-' + dmg, e.x, e.y - 20, '#ff8080');
soundHit();
}
}
});
}
// Ranged bow (right click / space when unlocked)
if (keys['k'] || (mouse.right && player.rangedUnlocked)) {
// This is handled by mouse.right event, but let's also allow K key
}
// Right click bow
if (mouse.right && player.rangedUnlocked) {
mouse.right = false; // consume
soundBow();
const ang = Math.atan2(mouse.y - H/2, mouse.x - W/2);
entities.push({
type: 'projectile',
x: player.x, y: player.y,
vx: Math.cos(ang) * 8,
vy: Math.sin(ang) * 8,
life: 120,
ang: ang,
});
}
// Update entities
for (let i = entities.length - 1; i >= 0; i--) {
const e = entities[i];
if (e.type === 'enemy') {
// Move toward player slowly
const dx = player.x - e.x, dy = player.y - e.y;
const dist = Math.hypot(dx, dy);
if (dist > 12) {
e.x += (dx / dist) * e.speed;
e.y += (dy / dist) * e.speed;
}
// Damage player
if (dist < 14 && player.invuln <= 0 && !player.inventory.some(i => i.passive && i.id === 'shield')) {
player.health -= 1;
player.invuln = 30;
spawnParticles(player.x, player.y, 10, '#ff5050');
spawnText('-1', player.x, player.y - 20, '#ff6060');
soundHit();
if (player.health <= 0) {
soundDeath();
gameState = 'dead';
document.getElementById('gameOverScreen').style.display = 'block';
}
}
// Enemy death
if (e.hp <= 0) {
spawnParticles(e.x, e.y, 14, '#ffa060');
spawnDrop(e.x, e.y);
entities.splice(i, 1);
}
} else if (e.type === 'drop') {
e.life--;
// Collect
const dist = Math.hypot(e.x - player.x, e.y - player.y);
if (dist < 22) {
// Add to inventory
const existing = player.inventory.find(i => i.id === e.item.id);
if (existing) existing.qty = (existing.qty || 1) + 1;
else player.inventory.push({ ...e.item, qty: 1 });
player.money += (e.item.price || 0);
soundPickup();
spawnParticles(e.x, e.y, 10, '#ffe066');
if (e.item.id === 'potion') spawnText('+Potion', e.x, e.y - 20, '#ff6b6b');
entities.splice(i, 1);
} else if (e.life <= 0) {
entities.splice(i, 1);
}
} else if (e.type === 'projectile') {
e.x += e.vx; e.y += e.vy;
e.life--;
// Hit enemy
for (let j = entities.length - 1; j >= 0; j--) {
const en = entities[j];
if (en.type === 'enemy') {
const dist = Math.hypot(e.x - en.x, e.y - en.y);
if (dist < 14) {
en.hp -= 10;
spawnParticles(e.x, e.y, 6, '#a0d0ff');
entities.splice(i, 1);
if (en.hp <= 0) {
spawnParticles(en.x, en.y, 14, '#ffa060');
spawnDrop(en.x, en.y);
entities.splice(j, 1);
}
break;
}
}
}
if (e.life <= 0) entities.splice(i, 1);
}
}
// Player invulnerability timer
if (player.invuln > 0) player.invuln--;
// Check save points
savePoints.forEach(s => {
const d = Math.hypot(s.x - player.x, s.y - player.y);
if (d < 30 && keys[' '] && s.active) {
lastSave = { x: s.x, y: s.y };
showSaveMsg();
soundSave();
keys[' '] = false;
}
});
// Check shop interaction
const dShop = Math.hypot(shopPos.x - player.x, shopPos.y - player.y);
if (dShop < 40) {
// Auto-show shop on space
if (keys[' '] || keys['enter']) {
openShop();
keys[' '] = false; keys['enter'] = false;
}
} else {
document.getElementById('shopUI').style.display = 'none';
}
// Progression triggers
// Mid-game: unlock bow when reached ~mid-progression (e.g., 3 kills or reached certain zone)
const kills = entities.filter(e => false); // not tracking kills directly, let's use zone entry
// Simplified: unlock bow when player reaches x > 500 once
if (!player.rangedUnlocked && player.x > 500) {
player.rangedUnlocked = true;
spawnText('Bow Unlocked!', player.x, player.y - 30, '#6bbfff');
soundUpgrade();
}
// Weapon upgrade triggers
// Upgrade 1 at 150 gold, Upgrade 2 at 300 gold + at least one item owned
if (player.weaponLevel === 0 && player.money >= 120) {
player.weaponLevel = 1;
player.money -= 120;
soundUpgrade();
spawnText('Upgraded!', player.x, player.y - 30, '#c0b0ff');
updateUI();
}
if (player.weaponLevel === 1 && player.money >= 250 && player.inventory.length > 0) {
player.weaponLevel = 2;
player.money -= 250;
soundUpgrade();
spawnParticles(player.x, player.y, 20, '#f0a080');
spawnText('Master Blade!', player.x, player.y - 30, '#e8c0a0');
updateUI();
}
}
// ======================= SHOP =======================
const shopInventory = [
{ itemId: 'potion', qty: 1, price: 30 },
{ itemId: 'shield', qty: 1, price: 50 },
{ itemId: 'ring', qty: 1, price: 40 },
{ itemId: 'crystal', qty: 1, price: 60 },
{ itemId: 'orb', qty: 1, price: 20 },
];
function openShop() {
initAudio();
document.getElementById('shopUI').style.display = 'block';
soundShop();
const container = document.getElementById('shopItems');
container.innerHTML = '';
shopInventory.forEach(s => {
const def = itemDefs.find(i => i.id === s.itemId);
if (!def) return;
const btn = document.createElement('button');
btn.className = 'shop-item';
btn.innerHTML = `<div class="icon" style="background:${def.icon}; color:#fff;">●</div>
<div class="info"><div class="name">${def.name}</div><div class="desc">${def.desc}</div></div>
<div class="price">${s.price} G</div>`;
btn.onclick = () => {
if (player.money >= s.price) {
player.money -= s.price;
const existing = player.inventory.find(i => i.id === def.id);
if (existing) existing.qty = (existing.qty || 1) + 1;
else player.inventory.push({ ...def, qty: 1 });
soundPickup();
spawnParticles(player.x, player.y, 10, '#ffe066');
updateUI();
btn.style.opacity = '0.5';
btn.innerHTML = btn.innerHTML.replace(s.price + ' G', 'SOLD');
}
};
container.appendChild(btn);
});
}
function closeShop() { document.getElementById('shopUI').style.display = 'none'; }
// ======================= UI =======================
function updateUI() {
// Hearts
let html = '';
const fullHearts = Math.floor(player.health / 2);
const half = (player.health % 2) === 1;
for (let i = 0; i < 3; i++) {
if (i < fullHearts) html += `<div class="heart full"><svg viewBox="0 0 24 20"><path d="M12 19s-6-5.5-6-12a6 6 0 0 1 12 0c0 6.5-6 12-6 12z"/></svg></div>`;
else if (i === fullHearts && half) html += `<div class="heart half"><svg viewBox="0 0 24 20"><path d="M12 19s-6-5.5-6-12a6 6 0 0 1 12 0c0 6.5-6 12-6 12z"/></svg></div>`;
else html += `<div class="heart empty"><svg viewBox="0 0 24 20"><path d="M12 19s-6-5.5-6-12a6 6 0 0 1 12 0c0 6.5-6 12-6 12z"/></svg></div>`;
}
document.getElementById('hearts').innerHTML = html;
// Money
document.getElementById('moneyText').textContent = player.money + ' G';
// Weapon
const w = weapons[player.weaponLevel];
document.getElementById('weaponName').textContent = w.name;
document.getElementById('weaponIcon').style.background = w.iconColor;
// Inventory bar
const invBar = document.getElementById('inventoryBar');
invBar.innerHTML = '';
for (let i = 0; i < 5; i++) {
const slot = document.createElement('div');
slot.className = 'inv-slot' + (i === 0 ? ' active' : '');
if (player.inventory[i]) {
const item = player.inventory[i];
slot.innerHTML = `<div class="inv-icon" style="background:${item.icon}; border:1px solid rgba(255,255,255,0.2);"></div>`;
if (item.qty > 1) slot.innerHTML += `<div class="qty">${item.qty}</div>`;
}
invBar.appendChild(slot);
}
}
function showSaveMsg() {
const msg = document.getElementById('saveMsg');
msg.style.opacity = '1';
setTimeout(() => msg.style.opacity = '0', 2000);
}
// ======================= INPUT =======================
window.addEventListener('keydown', e => {
keys[e.key.toLowerCase()] = true;
if (['1','2','3','4','5'].includes(e.key)) {
const idx = parseInt(e.key) - 1;
if (player.inventory[idx]) {
player.inventory[idx].use();
if (player.inventory[idx].qty <= 0 || (player.inventory[idx].qty === undefined)) {
// Remove if no qty or qty depleted
// Actually keep for consumables, reduce qty
if (player.inventory[idx].qty > 1) player.inventory[idx].qty--;
else player.inventory.splice(idx, 1);
updateUI();
}
updateUI();
}
}
});
window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
window.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
window.addEventListener('mousedown', e => {
initAudio();
mouse.down = true;
if (e.button === 2) mouse.right = true;
});
window.addEventListener('mouseup', e => {
mouse.down = false;
mouse.right = false;
});
window.addEventListener('contextmenu', e => e.preventDefault());
// ======================= INIT & LOOP =======================
let lastTime = 0;
function loop(timestamp) {
const dt = timestamp - lastTime; lastTime = timestamp;
update();
drawWorld();
requestAnimationFrame(loop);
}
function startGame() {
initAudio();
gameState = 'playing';
document.getElementById('startScreen').style.display = 'none';
document.getElementById('gameOverScreen').style.display = 'none';
document.getElementById('shopUI').style.display = 'none';
resize();
// Reset state
player.x = 420; player.y = 340;
player.vx = 0; player.vy = 0;
player.health = 3;
player.maxHealth = 3; // Progresses to 5 through gameplay
player.money = 0;
player.weaponLevel = 0;
player.rangedUnlocked = false;
player.inventory = [];
player.invuln = 0;
player.attackCooldown = 0;
player.attackAnim = 0;
entities = [];
particles = [];
textFloats = [];
// Place initial entities
entities.push({ type: 'enemy', x: 250, y: 180, hp: 12, maxHp: 12, speed: 0.9, timer: 0 });
entities.push({ type: 'enemy', x: 800, y: 150, hp: 15, maxHp: 15, speed: 0.7, timer: 0 });
entities.push({ type: 'enemy', x: 500, y: 500, hp: 18, maxHp: 18, speed: 1.0, timer: 0 });
entities.push({ type: 'enemy', x: 150, y: 550, hp: 10, maxHp: 10, speed: 1.1, timer: 0 });
entities.push({ type: 'enemy', x: 850, y: 650, hp: 20, maxHp: 20, speed: 0.6, timer: 0 });
// Place drops
entities.push({ type: 'drop', x: 300, y: 120, item: itemDefs.find(i => i.id === 'potion'), itemColor: '#ff6b6b', life: 999 });
entities.push({ type: 'drop', x: 650, y: 200, item: itemDefs.find(i => i.id === 'shield'), itemColor: '#6bbfff', life: 999 });
entities.push({ type: 'drop', x: 550, y: 580, item: itemDefs.find(i => i.id === 'ring'), itemColor: '#ffe066', life: 999 });
// Heart Shard for health progression (earn max health to reach 5)
entities.push({ type: 'drop', x: 820, y: 300, item: itemDefs.find(i => i.id === 'heartshard'), itemColor: '#ff3b5a', life: 999 });
updateUI();
loop(0);
}
// Pre-render title
resize();
ctx.fillStyle = '#0b0c10';
ctx.fillRect(0,0,W,H);
// Title visual
ctx.save();
ctx.translate(W/2, H/2 - 40);
ctx.fillStyle = '#e8dcc8';
ctx.font = 'bold 52px "Cinzel Decorative", serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = '#c0a060';
ctx.shadowBlur = 20;
ctx.fillText('SHARD OF ETERNITY', 0, 0);
ctx.restore();
updateUI(); // Initialize empty health
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"f7f224a7-ff08-457b-a13c-dc262ee5c440","tokensIn":365252,"tokensOut":17021,"tokensTotal":382273,"turns":11,"toolCalls":10,"failedToolCalls":0,"timestamp":"2026-07-18T22:36:27.845Z"} -->