Aethelgard
Gemma 4 31b · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
Human review · Jul 16, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
- Visual design
- 4 / 5
- UX / feel
- 3 / 5
- Works
- 3 / 5
- Prompt spirit
- 4 / 5
- Delight
- 3 / 5
Excellent design; original creature svg artwork; some glitchy controls and no way to use potions
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 40,557 bytes · SHA-256 62b5c0919b08
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aethelgard: Resonance</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
:root {
--color-pyro: #ff4d4d;
--color-hydro: #4da6ff;
--color-flora: #4dff88;
--color-zephyr: #e6e6e6;
--color-earth: #daa520;
--color-volt: #ffff4d;
--color-cosmos: #bc4dff;
--glass: rgba(15, 23, 42, 0.7);
}
body {
font-family: 'Inter', sans-serif;
background: radial-gradient(circle at center, #1e1b4b 0%, #0f172a 100%);
color: #f1f5f9;
overflow: hidden;
user-select: none;
}
.retro-font { font-family: 'Press Start 2P', cursive; }
.glass-panel {
background: var(--glass);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.8);
}
.element-glow-pyro { border-color: var(--color-pyro); box-shadow: 0 0 15px var(--color-pyro); }
.element-glow-hydro { border-color: var(--color-hydro); box-shadow: 0 0 15px var(--color-hydro); }
.element-glow-flora { border-color: var(--color-flora); box-shadow: 0 0 15px var(--color-flora); }
.element-glow-zephyr { border-color: var(--color-zephyr); box-shadow: 0 0 15px var(--color-zephyr); }
.element-glow-earth { border-color: var(--color-earth); box-shadow: 0 0 15px var(--color-earth); }
.element-glow-volt { border-color: var(--color-volt); box-shadow: 0 0 15px var(--color-volt); }
.element-glow-cosmos { border-color: var(--color-cosmos); box-shadow: 0 0 15px var(--color-cosmos); }
.hp-bar-inner { transition: width 0.5s ease-out, background-color 0.3s ease; }
@keyframes bounce-gentle {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.animate-bounce-gentle { animation: bounce-gentle 3s infinite ease-in-out; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.screen-shake { animation: shake 0.2s ease-in-out 0s 2; }
.node-active { cursor: pointer; filter: drop-shadow(0 0 8px rgba(255,255,255,0.5)); transition: 0.3s; }
.node-active:hover { transform: scale(1.2); filter: drop-shadow(0 0 12px white); }
.node-locked { opacity: 0.4; cursor: not-allowed; }
.fade-in { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
</head>
<body>
<div id="app" class="h-screen w-screen relative overflow-hidden flex items-center justify-center p-4">
<!-- Screens will be injected here by the Game State Manager -->
</div>
<script>
/**
* WEB AUDIO SYNTHESIZER
*/
class SoundEngine {
constructor() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
play(type) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
const now = this.ctx.currentTime;
switch(type) {
case 'hover':
osc.type = 'sine';
osc.frequency.setValueAtTime(440, now);
osc.frequency.exponentialRampToValueAtTime(880, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(); osc.stop(now + 0.1);
break;
case 'select':
osc.type = 'square';
osc.frequency.setValueAtTime(523.25, now);
osc.frequency.exponentialRampToValueAtTime(1046.5, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(); osc.stop(now + 0.1);
break;
case 'hit':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(50, now + 0.2);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0, now + 0.2);
osc.start(); osc.stop(now + 0.2);
break;
case 'crit':
osc.type = 'triangle';
osc.frequency.setValueAtTime(800, now);
osc.frequency.linearRampToValueAtTime(1200, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0, now + 0.2);
osc.start(); osc.stop(now + 0.2);
break;
case 'levelup':
const now2 = this.ctx.currentTime;
[440, 554, 659, 880].forEach((f, i) => {
const o = this.ctx.createOscillator();
const g = this.ctx.createGain();
o.connect(g); g.connect(this.ctx.destination);
o.frequency.setValueAtTime(f, now2 + i*0.1);
g.gain.setValueAtTime(0.1, now2 + i*0.1);
g.gain.exponentialRampToValueAtTime(0.01, now2 + i*0.1 + 0.2);
o.start(now2 + i*0.1); o.stop(now2 + i*0.1 + 0.2);
});
break;
case 'capture':
osc.type = 'sine';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(2000, now + 0.5);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
osc.start(); osc.stop(now + 0.5);
break;
}
}
}
const FX = new SoundEngine();
/**
* DATABASE: RESONANTS
*/
const TYPE_CHART = {
Pyro: { strong: 'Flora', weak: 'Hydro' },
Hydro: { strong: 'Pyro', weak: 'Flora' },
Flora: { strong: 'Hydro', weak: 'Pyro' },
Zephyr: { strong: 'Earth', weak: 'Volt' },
Earth: { strong: 'Volt', weak: 'Zephyr' },
Volt: { strong: 'Zephyr', weak: 'Earth' },
Cosmos: { strong: null, weak: null }
};
const RESONANTS_DB = [
// Pyro Line
{ id: 'pyro1', name: 'Emberkit', type: 'Pyro', hp: 45, atk: 12, def: 8, spd: 10, evol: 'pyro2', evolLvl: 8, color: '#ff4d4d', paths: 'M20,80 L50,20 L80,80 Z M40,60 A10,10 0 1,0 60,60 A10,10 0 1,0 40,60',
moves: [{ name: 'Spark', pwr: 40, acc: 95, type: 'Pyro', lvl: 1 }, { name: 'Flare', pwr: 60, acc: 90, type: 'Pyro', lvl: 5 }, { name: 'Singe', pwr: 80, acc: 80, type: 'Pyro', lvl: 10 }, { name: 'Inferno', pwr: 110, acc: 75, type: 'Pyro', lvl: 15 }] },
{ id: 'pyro2', name: 'Pyrowolf', type: 'Pyro', hp: 70, atk: 18, def: 14, spd: 18, evol: 'pyro3', evolLvl: 15, color: '#ff774d', paths: 'M20,80 L30,40 L50,50 L70,40 L80,80 Z M40,40 L40,30 M60,40 L60,30',
moves: [{ name: 'Spark', pwr: 40, acc: 95, type: 'Pyro', lvl: 1 }, { name: 'Flare', pwr: 60, acc: 90, type: 'Pyro', lvl: 5 }, { name: 'Singe', pwr: 80, acc: 80, type: 'Pyro', lvl: 10 }, { name: 'Inferno', pwr: 110, acc: 75, type: 'Pyro', lvl: 15 }] },
{ id: 'pyro3', name: 'Ignisaur', type: 'Pyro', hp: 110, atk: 25, def: 20, spd: 20, evol: null, color: '#ff0000', paths: 'M10,80 Q50,10 90,80 Z M30,60 L40,50 M60,60 L70,50',
moves: [{ name: 'Spark', pwr: 40, acc: 95, type: 'Pyro', lvl: 1 }, { name: 'Flare', pwr: 60, acc: 90, type: 'Pyro', lvl: 5 }, { name: 'Singe', pwr: 80, acc: 80, type: 'Pyro', lvl: 10 }, { name: 'Inferno', pwr: 110, acc: 75, type: 'Pyro', lvl: 15 }] },
// Hydro Line
{ id: 'hydro1', name: 'Droplet', type: 'Hydro', hp: 50, atk: 10, def: 10, spd: 8, evol: 'hydro2', evolLvl: 8, color: '#4da6ff', paths: 'M50,20 Q80,50 80,80 Q50,90 20,80 Q20,50 50,20 Z',
moves: [{ name: 'Mist', pwr: 40, acc: 95, type: 'Hydro', lvl: 1 }, { name: 'Geyser', pwr: 60, acc: 90, type: 'Hydro', lvl: 5 }, { name: 'Tide', pwr: 80, acc: 80, type: 'Hydro', lvl: 10 }, { name: 'Tsunami', pwr: 110, acc: 75, type: 'Hydro', lvl: 15 }] },
{ id: 'hydro2', name: 'Geoxus', type: 'Hydro', hp: 80, atk: 16, def: 16, spd: 12, evol: 'hydro3', evolLvl: 15, color: '#4dceff', paths: 'M20,80 Q20,30 50,30 Q80,30 80,80 L60,70 L50,80 L40,70 Z',
moves: [{ name: 'Mist', pwr: 40, acc: 95, type: 'Hydro', lvl: 1 }, { name: 'Geyser', pwr: 60, acc: 90, type: 'Hydro', lvl: 5 }, { name: 'Tide', pwr: 80, acc: 80, type: 'Hydro', lvl: 10 }, { name: 'Tsunami', pwr: 110, acc: 75, type: 'Hydro', lvl: 15 }] },
{ id: 'hydro3', name: 'Neptunian', type: 'Hydro', hp: 120, atk: 22, def: 22, spd: 15, evol: null, color: '#004dff', paths: 'M10,70 Q50,10 90,70 L80,90 L50,80 L20,90 Z',
moves: [{ name: 'Mist', pwr: 40, acc: 95, type: 'Hydro', lvl: 1 }, { name: 'Geyser', pwr: 60, acc: 90, type: 'Hydro', lvl: 5 }, { name: 'Tide', pwr: 80, acc: 80, type: 'Hydro', lvl: 10 }, { name: 'Tsunami', pwr: 110, acc: 75, type: 'Hydro', lvl: 15 }] },
// Flora Line
{ id: 'flora1', name: 'Seedling', type: 'Flora', hp: 40, atk: 11, def: 11, spd: 9, evol: 'flora2', evolLvl: 8, color: '#4dff88', paths: 'M50,80 L40,60 Q50,50 60,60 L50,80 M50,60 L50,40',
moves: [{ name: 'Leaf', pwr: 40, acc: 95, type: 'Flora', lvl: 1 }, { name: 'Root', pwr: 60, acc: 90, type: 'Flora', lvl: 5 }, { name: 'Vine', pwr: 80, acc: 80, type: 'Flora', lvl: 10 }, { name: 'Overgrow', pwr: 110, acc: 75, type: 'Flora', lvl: 15 }] },
{ id: 'flora2', name: 'Thornback', type: 'Flora', hp: 75, atk: 17, def: 17, spd: 11, evol: 'flora3', evolLvl: 15, color: '#77ff88', paths: 'M20,80 L30,50 L50,60 L70,50 L80,80 Z M50,50 L50,30',
moves: [{ name: 'Leaf', pwr: 40, acc: 95, type: 'Flora', lvl: 1 }, { name: 'Root', pwr: 60, acc: 90, type: 'Flora', lvl: 5 }, { name: 'Vine', pwr: 80, acc: 80, type: 'Flora', lvl: 10 }, { name: 'Overgrow', pwr: 110, acc: 75, type: 'Flora', lvl: 15 }] },
{ id: 'flora3', name: 'Bloomgoliath', type: 'Flora', hp: 130, atk: 24, def: 24, spd: 10, evol: null, color: '#00ff55', paths: 'M20,80 Q50,20 80,80 Z M40,40 A5,5 0 1,0 41,40 M60,40 A5,5 0 1,0 61,40',
moves: [{ name: 'Leaf', pwr: 40, acc: 95, type: 'Flora', lvl: 1 }, { name: 'Root', pwr: 60, acc: 90, type: 'Flora', lvl: 5 }, { name: 'Vine', pwr: 80, acc: 80, type: 'Flora', lvl: 10 }, { name: 'Overgrow', pwr: 110, acc: 75, type: 'Flora', lvl: 15 }] },
// Volt Line
{ id: 'volt1', name: 'Sparky', type: 'Volt', hp: 35, atk: 15, def: 7, spd: 16, evol: 'volt2', evolLvl: 8, color: '#ffff4d', paths: 'M50,20 L60,40 L80,40 L60,60 L70,80 L50,60 L30,80 L40,60 L20,40 L40,40 Z',
moves: [{ name: 'Zap', pwr: 40, acc: 95, type: 'Volt', lvl: 1 }, { name: 'Bolt', pwr: 60, acc: 90, type: 'Volt', lvl: 5 }, { name: 'Shock', pwr: 80, acc: 80, type: 'Volt', lvl: 10 }, { name: 'Thunder', pwr: 110, acc: 75, type: 'Volt', lvl: 15 }] },
{ id: 'volt2', name: 'Boltsaber', type: 'Volt', hp: 65, atk: 21, def: 12, spd: 22, evol: 'volt3', evolLvl: 15, color: '#ffff88', paths: 'M30,80 L50,20 L70,80 L50,70 Z M50,20 L50,10',
moves: [{ name: 'Zap', pwr: 40, acc: 95, type: 'Volt', lvl: 1 }, { name: 'Bolt', pwr: 60, acc: 90, type: 'Volt', lvl: 5 }, { name: 'Shock', pwr: 80, acc: 80, type: 'Volt', lvl: 10 }, { name: 'Thunder', pwr: 110, acc: 75, type: 'Volt', lvl: 15 }] },
{ id: 'volt3', name: 'Stormlord', type: 'Volt', hp: 90, atk: 28, def: 18, spd: 28, evol: null, color: '#ffff00', paths: 'M20,80 L40,40 L50,60 L60,40 L80,80 Z M50,40 L50,10',
moves: [{ name: 'Zap', pwr: 40, acc: 95, type: 'Volt', lvl: 1 }, { name: 'Bolt', pwr: 60, acc: 90, type: 'Volt', lvl: 5 }, { name: 'Shock', pwr: 80, acc: 80, type: 'Volt', lvl: 10 }, { name: 'Thunder', pwr: 110, acc: 75, type: 'Volt', lvl: 15 }] },
// Zephyr Line
{ id: 'zeph1', name: 'Gusty', type: 'Zephyr', hp: 40, atk: 12, def: 8, spd: 15, evol: 'zeph2', evolLvl: 8, color: '#e6e6e6', paths: 'M50,20 Q70,50 50,80 Q30,50 50,20 Z M30,50 L70,50',
moves: [{ name: 'Breeze', pwr: 40, acc: 95, type: 'Zephyr', lvl: 1 }, { name: 'Gale', pwr: 60, acc: 90, type: 'Zephyr', lvl: 5 }, { name: 'Sirocco', pwr: 80, acc: 80, type: 'Zephyr', lvl: 10 }, { name: 'Cyclone', pwr: 110, acc: 75, type: 'Zephyr', lvl: 15 }] },
{ id: 'zeph2', name: 'Slipstream', type: 'Zephyr', hp: 70, atk: 18, def: 13, spd: 21, evol: 'zeph3', evolLvl: 15, color: '#ffffff', paths: 'M20,60 Q50,20 80,60 Q50,90 20,60 Z M40,60 L60,60',
moves: [{ name: 'Breeze', pwr: 40, acc: 95, type: 'Zephyr', lvl: 1 }, { name: 'Gale', pwr: 60, acc: 90, type: 'Zephyr', lvl: 5 }, { name: 'Sirocco', pwr: 80, acc: 80, type: 'Zephyr', lvl: 10 }, { name: 'Cyclone', pwr: 110, acc: 75, type: 'Zephyr', lvl: 15 }] },
{ id: 'zeph3', name: 'Zephyrus', type: 'Zephyr', hp: 100, atk: 24, def: 18, spd: 28, evol: null, color: '#f0f8ff', paths: 'M10,50 Q50,10 90,50 Q50,90 10,50 Z M30,50 L70,50',
moves: [{ name: 'Breeze', pwr: 40, acc: 95, type: 'Zephyr', lvl: 1 }, { name: 'Gale', pwr: 60, acc: 90, type: 'Zephyr', lvl: 5 }, { name: 'Sirocco', pwr: 80, acc: 80, type: 'Zephyr', lvl: 10 }, { name: 'Cyclone', pwr: 110, acc: 75, type: 'Zephyr', lvl: 15 }] },
// Earth Line
{ id: 'earth1', name: 'Pebble', type: 'Earth', hp: 60, atk: 11, def: 15, spd: 5, evol: 'earth2', evolLvl: 8, color: '#daa520', paths: 'M30,80 L70,80 L60,50 L40,50 Z',
moves: [{ name: 'Slam', pwr: 40, acc: 95, type: 'Earth', lvl: 1 }, { name: 'Tremor', pwr: 60, acc: 90, type: 'Earth', lvl: 5 }, { name: 'Quake', pwr: 80, acc: 80, type: 'Earth', lvl: 10 }, { name: 'Bedrock', pwr: 110, acc: 75, type: 'Earth', lvl: 15 }] },
{ id: 'earth2', name: 'Obsidian', type: 'Earth', hp: 90, atk: 17, def: 20, spd: 8, evol: 'earth3', evolLvl: 15, color: '#b8860b', paths: 'M20,80 L80,80 L70,40 L30,40 Z M40,40 L40,30 M60,40 L60,30',
moves: [{ name: 'Slam', pwr: 40, acc: 95, type: 'Earth', lvl: 1 }, { name: 'Tremor', pwr: 60, acc: 90, type: 'Earth', lvl: 5 }, { name: 'Quake', pwr: 80, acc: 80, type: 'Earth', lvl: 10 }, { name: 'Bedrock', pwr: 110, acc: 75, type: 'Earth', lvl: 15 }] },
{ id: 'earth3', name: 'Terrasunder', type: 'Earth', hp: 150, atk: 23, def: 28, spd: 10, evol: null, color: '#8b4513', paths: 'M10,80 L90,80 L80,30 L20,30 Z M30,30 L30,20 M50,30 L50,20 M70,30 L70,20',
moves: [{ name: 'Slam', pwr: 40, acc: 95, type: 'Earth', lvl: 1 }, { name: 'Tremor', pwr: 60, acc: 90, type: 'Earth', lvl: 5 }, { name: 'Quake', pwr: 80, acc: 80, type: 'Earth', lvl: 10 }, { name: 'Bedrock', pwr: 110, acc: 75, type: 'Earth', lvl: 15 }] },
];
/**
* GAME STATE & ENGINE
*/
const STATE = {
screen: 'MENU',
player: {
shards: 500,
party: [],
inventory: { prisms: 5, hyperPrisms: 0, elixirs: 2, restorers: 2 },
unlockedZones: 1,
currentZone: 1,
currentNode: 0,
defeatedBosses: []
},
battle: null,
zones: [
{ name: 'The Verdant Canopy', color: '#4dff88', nodes: 5, types: ['Flora', 'Zephyr'], boss: { id: 'flora3', name: 'Bloomgoliath Prime', lvl: 5, dialogue: 'Nature shall reclaim this land!' } },
{ name: 'The Sunken Grotto', color: '#4da6ff', nodes: 5, types: ['Hydro', 'Earth'], boss: { id: 'hydro3', name: 'Neptunian Rex', lvl: 10, dialogue: 'Drown in the depths of Aethelgard!' } },
{ name: 'The Obsidian Crags', color: '#ff4d4d', nodes: 5, types: ['Pyro', 'Volt'], boss: { id: 'pyro3', name: 'Ignisaur Alpha', lvl: 15, dialogue: 'Everything turns to ash eventually!' } },
{ name: 'The Apex Pillar', color: '#bc4dff', nodes: 3, types: ['Cosmos', 'Pyro', 'Hydro', 'Flora', 'Zephyr', 'Earth', 'Volt'], boss: { id: 'pyro3', name: 'The Harmonic Core', lvl: 20, dialogue: 'I am the vibration of the universe itself!' } },
]
};
class GameEngine {
constructor() {
this.init();
}
init() {
this.load();
if (STATE.player.party.length === 0) {
this.giveStarter();
}
this.render();
}
giveStarter() {
const starters = [RESONANTS_DB[0], RESONANTS_DB[3], RESONANTS_DB[6]];
// Default to Pyro for first start
const s = JSON.parse(JSON.stringify(starters[0]));
s.currentHp = s.hp;
s.lvl = 5;
s.xp = 0;
STATE.player.party.push(s);
this.save();
}
save() {
localStorage.setItem('aethelgard_save', JSON.stringify(STATE.player));
}
load() {
const saved = localStorage.getItem('aethelgard_save');
if (saved) STATE.player = JSON.parse(saved);
}
reset() {
localStorage.removeItem('aethelgard_save');
location.reload();
}
setScreen(screen) {
STATE.screen = screen;
this.render();
}
// --- Combat Logic ---
startBattle(isBoss = false, bossData = null) {
const wildResonant = this.generateWildResonant();
const enemy = bossData ? { ...this.getDb(bossData.id), lvl: bossData.lvl, currentHp: 0, name: bossData.name } : wildResonant;
if (!bossData) {
enemy.currentHp = enemy.hp;
} else {
enemy.currentHp = enemy.hp * (enemy.lvl / 5);
}
STATE.battle = {
playerResonant: STATE.player.party[0],
enemy: enemy,
turn: 'player',
isBoss: isBoss,
log: 'A wild Resonant appeared!',
bossDialogue: bossData ? bossData.dialogue : null
};
this.setScreen('BATTLE');
}
generateWildResonant() {
const zone = STATE.zones[STATE.player.currentZone - 1];
const type = zone.types[Math.floor(Math.random() * zone.types.length)];
const possible = RESONANTS_DB.filter(r => r.type === type);
const base = possible[Math.floor(Math.random() * possible.length)];
const lvl = Math.max(1, Math.floor(Math.random() * 5) + (STATE.player.currentZone * 4));
const res = JSON.parse(JSON.stringify(base));
res.lvl = lvl;
res.currentHp = res.hp + (lvl * 5);
res.maxHp = res.currentHp;
return res;
}
getDb(id) { return RESONANTS_DB.find(r => r.id === id); }
calculateDamage(attacker, defender, move) {
const levelMod = (2 * attacker.lvl / 5 + 2);
const ratio = attacker.atk / defender.def;
let damage = (((levelMod * move.pwr * ratio) / 50) + 2);
// Type Modifier
const atkType = move.type;
const defType = defender.type;
let mod = 1;
if (TYPE_CHART[atkType] && TYPE_CHART[atkType].strong === defType) mod = 1.5;
if (TYPE_CHART[atkType] && TYPE_CHART[atkType].weak === defType) mod = 0.5;
damage *= mod;
const crit = Math.random() < 0.1 ? 1.5 : 1;
damage *= crit * (0.85 + Math.random() * 0.15);
return {
value: Math.floor(damage),
isCrit: crit > 1,
isSuper: mod > 1
};
}
async executeTurn(action, moveIndex = null) {
const b = STATE.battle;
const p = b.playerResonant;
const e = b.enemy;
if (action === 'attack') {
const move = p.moves[moveIndex];
if (Math.random() * 100 > move.acc) {
b.log = `The attack missed!`;
} else {
constdmg = this.calculateDamage(p, e, move);
e.currentHp -= constdmg.value;
b.log = `${p.name} used ${move.name}! ${constdmg.isCrit ? 'CRITICAL HIT! ' : ''}${constdmg.isSuper ? 'It\'s super effective! ' : ''}Dealt ${constdmg.value} damage.`;
FX.play(constdmg.isCrit ? 'crit' : 'hit');
}
} else if (action === 'prism') {
const prismType = STATE.player.inventory.hyperPrisms > 0 ? 'hyper' : 'basic';
if (STATE.player.inventory.prisms <= 0 && STATE.player.inventory.hyperPrisms <= 0) {
b.log = "No Prisms remaining!";
this.render();
return;
}
const rate = (1 - (e.currentHp / e.maxHp)) * 0.8 + (prismType === 'hyper' ? 0.4 : 0.1);
if (Math.random() < rate) {
FX.play('capture');
const captured = JSON.parse(JSON.stringify(e));
captured.currentHp = captured.hp;
STATE.player.party.push(captured);
b.log = `Success! ${e.name} was captured!`;
this.render();
setTimeout(() => {
this.save();
this.setScreen('MAP');
}, 2000);
return;
} else {
b.log = "The Resonant broke free!";
}
}
this.render();
// Enemy Turn
await new Promise(r => setTimeout(r, 1500));
if (e.currentHp <= 0) {
this.winBattle();
return;
}
const eMove = e.moves[Math.floor(Math.random() * e.moves.length)];
const dmg = this.calculateDamage(e, p, eMove);
p.currentHp -= dmg.value;
b.log = `${e.name} used ${eMove.name}! Dealt ${dmg.value} damage.`;
FX.play('hit');
if (p.currentHp <= 0) {
b.log = "Your Resonant fainted! Returning to map...";
this.render();
setTimeout(() => this.setScreen('MAP'), 2000);
}
this.render();
}
winBattle() {
const e = STATE.battle.enemy;
const xpGain = e.lvl * 20;
const shardGain = e.lvl * 10;
STATE.player.party[0].xp += xpGain;
STATE.player.shards += shardGain;
// Level Up
let p = STATE.player.party[0];
while (p.xp >= p.lvl * 100) {
p.xp -= p.lvl * 100;
p.lvl++;
p.hp += 10;
p.atk += 2;
p.def += 2;
FX.play('levelup');
}
// Evolution
const dbEntry = this.getDb(p.id);
if (dbEntry.evol && p.lvl >= dbEntry.evolLvl) {
const next = this.getDb(dbEntry.evol);
p.id = next.id;
p.name = next.name;
p.type = next.type;
p.hp = next.hp + (p.lvl * 5);
p.atk = next.atk + (p.lvl * 2);
p.def = next.def + (p.lvl * 2);
p.color = next.color;
p.paths = next.paths;
b.log = `What? ${p.name} is evolving!`;
}
STATE.battle.log = `Victory! Gained ${xpGain} XP and ${shardGain} Shards.`;
this.render();
if (STATE.battle.isBoss) {
const zoneIdx = STATE.player.currentZone - 1;
STATE.player.unlockedZones = Math.min(4, STATE.player.unlockedZones + 1);
STATE.player.defeatedBosses.push(STATE.zones[zoneIdx].name);
}
setTimeout(() => {
this.save();
this.setScreen('MAP');
}, 3000);
}
// --- Rendering Engine ---
render() {
const app = document.getElementById('app');
app.innerHTML = '';
if (STATE.screen === 'MENU') {
app.innerHTML = `
<div class="fade-in flex flex-col items-center gap-8 text-center">
<h1 class="retro-font text-4xl md:text-6xl text-white mb-4 drop-shadow-lg">AETHELGARD<br><span class="text-2xl text-indigo-400">RESONANCE</span></h1>
<div class="flex flex-col gap-4 w-64">
<button onclick="game.setScreen('MAP')" class="glass-panel p-4 rounded-xl hover:bg-white/10 transition-all retro-font text-sm">START JOURNEY</button>
<button onclick="game.setScreen('DEX')" class="glass-panel p-4 rounded-xl hover:bg-white/10 transition-all retro-font text-sm">CONDUCTOR DEX</button>
<button onclick="game.reset()" class="glass-panel p-4 rounded-xl hover:bg-red-900/20 transition-all retro-font text-sm text-red-400">RESET DATA</button>
</div>
</div>
`;
} else if (STATE.screen === 'MAP') {
const zone = STATE.zones[STATE.player.currentZone - 1];
app.innerHTML = `
<div class="fade-in w-full max-w-4xl h-full flex flex-col p-6">
<div class="flex justify-between items-center mb-8">
<div>
<h2 class="retro-font text-xl text-white">${zone.name}</h2>
<p class="text-slate-400 text-sm">Zone ${STATE.player.currentZone} / 4</p>
</div>
<div class="glass-panel px-4 py-2 rounded-full flex gap-4 text-sm font-semibold">
<span>💎 ${STATE.player.shards}</span>
<span>🎒 Prisms: ${STATE.player.inventory.prisms + STATE.player.inventory.hyperPrisms}</span>
</div>
<button onclick="game.setScreen('SHOP')" class="glass-panel px-4 py-2 rounded-lg hover:bg-white/10 transition-all text-sm retro-font">SHOP</button>
</div>
<div class="relative flex-1 flex items-center justify-center">
<svg width="800" height="400" viewBox="0 0 800 400" class="overflow-visible">
<!-- Path Line -->
<polyline points="100,200 200,150 300,250 400,150 500,200 600,150 700,200" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="4" stroke-dasharray="10,10" />
${Array.from({length: 7}).map((_, i) => {
const points = [[100,200], [200,150], [300,250], [400,150], [500,200], [600,150], [700,200]];
const [x, y] = points[i];
const isLocked = i > STATE.player.currentNode + 1;
const isCurrent = i === STATE.player.currentNode;
const isBoss = i === 6;
return `
<g class="${isLocked ? 'node-locked' : 'node-active'}" onclick="game.handleNodeClick(${i})">
<circle cx="${x}" cy="${y}" r="20" fill="${isBoss ? '#bc4dff' : zone.color}" class="${isCurrent ? 'ring-4 ring-white' : ''}" />
<text x="${x}" y="${y+35}" text-anchor="middle" fill="white" class="text-xs font-bold">${isBoss ? 'GATEKEEPER' : 'NODE ' + (i+1)}</text>
</g>
`;
}).join('')}
</svg>
</div>
<div class="mt-8 glass-panel p-4 rounded-2xl text-center retro-font text-xs text-slate-300">
Click a node to explore. Defeat the Gatekeeper to unlock the next zone.
</div>
</div>
`;
} else if (STATE.screen === 'BATTLE') {
const b = STATE.battle;
const p = b.playerResonant;
const e = b.enemy;
app.innerHTML = `
<div class="fade-in w-full max-w-5xl h-full flex flex-col justify-between p-4 gap-4">
<div class="flex justify-between items-start">
<!-- Player Status -->
<div class="glass-panel p-4 rounded-2xl w-64 element-glow-${p.type.toLowerCase()}">
<div class="flex justify-between items-end mb-2">
<span class="retro-font text-xs">${p.name}</span>
<span class="text-xs">Lv.${p.lvl}</span>
</div>
<div class="w-full h-3 bg-slate-800 rounded-full overflow-hidden border border-white/10">
<div class="hp-bar-inner h-full" style="width: ${(p.currentHp/p.maxHp)*100}%; background-color: ${this.getHpColor(p.currentHp/p.maxHp)}"></div>
</div>
<div class="text-right text-[10px] mt-1">${Math.floor(p.currentHp)} / ${p.maxHp} HP</div>
</div>
<!-- Enemy Status -->
<div class="glass-panel p-4 rounded-2xl w-64 element-glow-${e.type.toLowerCase()} text-right">
<div class="flex justify-between items-end mb-2 flex-row-reverse">
<span class="retro-font text-xs">${e.name}</span>
<span class="text-xs">Lv.${e.lvl}</span>
</div>
<div class="w-full h-3 bg-slate-800 rounded-full overflow-hidden border border-white/10">
<div class="hp-bar-inner h-full ml-auto" style="width: ${(e.currentHp/e.maxHp)*100}%; background-color: ${this.getHpColor(e.currentHp/e.maxHp)}"></div>
</div>
<div class="text-left text-[10px] mt-1">${Math.floor(e.currentHp)} / ${e.maxHp} HP</div>
</div>
</div>
<div class="flex justify-between items-center px-12">
<div class="w-48 h-48 flex items-center justify-center animate-bounce-gentle">
${this.renderResonant(p)}
</div>
<div class="w-48 h-48 flex items-center justify-center animate-bounce-gentle" style="animation-delay: 1.5s">
${this.renderResonant(e)}
</div>
</div>
<div class="glass-panel p-6 rounded-3xl flex flex-col gap-4">
<div class="h-12 text-center retro-font text-sm text-indigo-300" id="battle-log">${b.log}</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-2">
${p.moves.map((m, i) => `
<button onclick="game.executeTurn('attack', ${i})" class="bg-white/10 p-2 rounded-lg hover:bg-white/20 transition-all text-xs font-bold">${m.name}</button>
`).join('')}
</div>
<div class="grid grid-cols-2 gap-2">
<button onclick="game.executeTurn('prism')" class="bg-indigo-600/40 p-2 rounded-lg hover:bg-indigo-600/60 transition-all text-xs font-bold">PRISM</button>
<button onclick="game.setScreen('MAP')" class="bg-slate-600/40 p-2 rounded-lg hover:bg-slate-600/60 transition-all text-xs font-bold">RUN</button>
</div>
</div>
</div>
</div>
`;
} else if (STATE.screen === 'SHOP') {
app.innerHTML = `
<div class="fade-in glass-panel p-8 rounded-3xl w-full max-w-2xl">
<h2 class="retro-font text-2xl text-center mb-8">CAMP SHOP</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
<div class="flex justify-between items-center p-4 bg-white/5 rounded-xl border border-white/10">
<span>Harmonizer Prism (Basic)</span>
<button onclick="game.buy('prisms', 100)" class="bg-indigo-600 px-3 py-1 rounded-lg text-xs retro-font">100💎</button>
</div>
<div class="flex justify-between items-center p-4 bg-white/5 rounded-xl border border-white/10">
<span>Hyper-Conductive Prism</span>
<button onclick="game.buy('hyperPrisms', 300)" class="bg-indigo-600 px-3 py-1 rounded-lg text-xs retro-font">300💎</button>
</div>
<div class="flex justify-between items-center p-4 bg-white/5 rounded-xl border border-white/10">
<span>Core Restorer (50% HP)</span>
<button onclick="game.buy('restorers', 150)" class="bg-indigo-600 px-3 py-1 rounded-lg text-xs retro-font">150💎</button>
</div>
<div class="flex justify-between items-center p-4 bg-white/5 rounded-xl border border-white/10">
<span>Resonance Elixir (Full Heal)</span>
<button onclick="game.buy('elixirs', 250)" class="bg-indigo-600 px-3 py-1 rounded-lg text-xs retro-font">250💎</button>
</div>
</div>
<button onclick="game.setScreen('MAP')" class="w-full p-4 bg-slate-700 rounded-xl retro-font text-sm">BACK TO MAP</button>
</div>
`;
} else if (STATE.screen === 'DEX') {
app.innerHTML = `
<div class="fade-in w-full max-w-6xl h-full flex flex-col p-6">
<h2 class="retro-font text-3xl text-center mb-8">CONDUCTOR DEX</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 overflow-y-auto p-4">
${RESONANTS_DB.map(r => `
<div class="glass-panel p-4 rounded-2xl text-center ${STATE.player.party.some(p => p.id === r.id) ? 'opacity-100' : 'opacity-40'}">
<div class="w-20 h-20 mx-auto mb-2">${this.renderResonant(r)}</div>
<div class="text-xs font-bold">${r.name}</div>
<div class="text-[10px] text-slate-400">${r.type}</div>
</div>
`).join('')}
</div>
<button onclick="game.setScreen('MENU')" class="mt-8 mx-auto w-64 p-4 glass-panel rounded-xl retro-font text-sm">BACK TO MENU</button>
</div>
`;
}
}
renderResonant(r) {
return `
<svg width="100" height="100" viewBox="0 0 100 100">
<defs>
<radialGradient id="grad-${r.id}" cx="50%" cy="50%" r="50%">
<stop offset="0%" style="stop-color:${r.color};stop-opacity:0.6" />
<stop offset="100%" style="stop-color:${r.color};stop-opacity:0" />
</radialGradient>
</defs>
<circle cx="50" cy="50" r="40" fill="url(#grad-${r.id})" />
<path d="${r.paths}" fill="none" stroke="${r.color}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
</svg>
`;
}
getHpColor(pct) {
if (pct > 0.5) return '#4dff88';
if (pct > 0.2) return '#ffff4d';
return '#ff4d4d';
}
handleNodeClick(index) {
if (index > STATE.player.currentNode + 1) return;
STATE.player.currentNode = index;
const zone = STATE.zones[STATE.player.currentZone - 1];
if (index === 6) {
this.startBattle(true, zone.boss);
} else {
this.startBattle();
}
}
buy(item, cost) {
if (STATE.player.shards >= cost) {
STATE.player.shards -= cost;
STATE.player.inventory[item]++;
FX.play('select');
this.save();
this.render();
}
}
}
const game = new GameEngine();
</script>
</body>
</html>
<!-- agent-meta {"model":"gemma-4-31b","provider":"cerebras","persona":"basic","sessionId":"4d8fd7b6-df8a-4e45-bfef-677347efa5b1","tokensIn":1837,"tokensOut":12776,"tokensTotal":14613,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-14T21:54:48.486Z"} -->